Initial commit

This commit is contained in:
mkpaz 2022-07-21 12:58:01 +04:00
commit 5169b3de7b
163 changed files with 20961 additions and 0 deletions

2
.gitattributes vendored Executable file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

17
.github/actions/prepare-java/action.yml vendored Normal file

@ -0,0 +1,17 @@
name: "Prepare Java"
description: "Install Java and cache Maven dependencies"
runs:
using: "composite"
steps:
- name: Build | Setup OpenJDK
uses: actions/setup-java@v1
with:
java-version: 17
- name: Build | Cache Maven dependencies
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2

74
.github/workflows/tagged-release.yml vendored Normal file

@ -0,0 +1,74 @@
name: Tagged Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- name: Prepare | Checkout
uses: actions/checkout@v2
- name: Prepare | Java
uses: ./.github/actions/prepare-java
- name: Extract release notes
id: extract-release-notes
uses: ffurrer2/extract-release-notes@v1
with:
release_notes_file: RELEASE_NOTES.md
- name: Build | Package
run: mvn -B clean install --file pom.xml
- name: Build | List artifacts (Unix)
shell: sh
run: ls -l ./sampler/target/release
if: matrix.os == 'ubuntu-latest'
- name: Build | List artifacts (Windows)
shell: pwsh
run: ls sampler\target\release
if: matrix.os == 'windows-latest'
- name: Build | Upload binaries
uses: actions/upload-artifact@v2
with:
name: binaries
path: ./sampler/target/release/*
retention-days: 1
- name: Build | Upload resources
uses: actions/upload-artifact@v2
with:
name: resources
path: |
./styles/**/*-themes.zip
RELEASE_NOTES.md
retention-days: 1
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Release | Download files
uses: actions/download-artifact@v2
- name: Release | List content
run: ls -R
- name: Release | Publish to Github
uses: softprops/action-gh-release@v1
with:
files: |
binaries/*
resources/**/*-themes.zip
body_path: resources/RELEASE_NOTES.md

370
.gitignore vendored Executable file

@ -0,0 +1,370 @@
# Created by https://www.toptal.com/developers/gitignore/api/maven,linux,eclipse,windows,intellij+all,grunt,node
# Edit at https://www.toptal.com/developers/gitignore?templates=maven,linux,eclipse,windows,intellij+all,grunt,node
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# CDT- autotools
.autotools
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Annotation Processing
.apt_generated/
.apt_generated_test/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
# Uncomment this line if you wish to ignore the project description file.
# Typically, this file would be tracked if it contains build/dependency configurations:
#.project
### Eclipse Patch ###
# Spring Boot Tooling
.sts4-cache/
### grunt ###
# Grunt usually compiles files inside this directory
dist/
# Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory
.tmp/
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/maven,linux,eclipse,windows,intellij+all,grunt,node
/node
/node_modules

18
.mvn/wrapper/maven-wrapper.properties vendored Normal file

@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

7
CHANGELOG.md Normal file

@ -0,0 +1,7 @@
# Changelog
## [Unreleased]
## [0.1]
Initial release.

21
LICENSE Executable file

@ -0,0 +1,21 @@
MIT License
Copyright (c) [2022] [mkpaz]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

52
README.md Normal file

@ -0,0 +1,52 @@
# AtlantaFX
JavaFX CSS theme collection plus additional controls. It's inspired by [FlatLaf](https://github.com/JFormDesigner/FlatLaf) and the variety of Web component frameworks.
---
![primer_light](./.screenshots/primer_light.png)
![primer_dark](./.screenshots/primer_dark.png)
## Installing
**Requirements:** JavaFX 17+ (because of `data-url` support).
TODO (after publishing on Maven Central)
## Introduction
TODO
## Motivation
**Goals**:
- SASS
JavaFX standard themes, namely Modena and Caspian, maintained as a huge single CSS file, which is an overwhelmingly hard task. This alone makes creating a new JavaFX theme from scratch hardly possible. Also JavaFX styling is based on CSS v2.1 specification which does not provide nor variables, nor mixins, nor modules nor any other goodies that are default for modern frontend development. AtlantaFX themes are written on SASS with each component in a separate module and use recent [Dart SASS](https://sass-lang.com/dart-sass) implementation for CSS compilation. It also follows [Github Primer](https://primer.style/design/foundations/color) color system to make creating new themes more simple.
- Additional controls
JavaFX 2.0 was started in 2011, and it introduced no additional controls since then. Some JavaFX controls are obsolete, some can be found in popular third-party libraries like [ControlsFX](https://github.com/controlsfx/controlsfx). The problem with the latter is that it provides much more than some missing controls. It provides many things that can be called a widget. That's why AtlantaFX borrows some existing controls from ControlsFX instead of supporting it directly. The rule of the thumb is to not re-invent any existing control from `javafx-controls` and to avoid widgets and everything that requires i18n support.
- Sampler application
Theme development is not possible without some kind of demo application where you can test each control under every angle. That's what the Sampler application is. It supports hot reload, thanks to [cssfx](https://github.com/McFoggy/cssfx), and you can observe the scene graph via [Scenic View](https://github.com/JonathanGiles/scenic-view).
- Distribution and flexibility
AtlantaFX is also distributed as a collection of CSS files. So, if you don't need additional controls, you can just download only CSS and use it via `Application.setUserAgentStylesheet()` method. If your application is only need a subset of controls, you can compile your own theme by just removing unnecessary components from SASS.
**Non-goals**:
- Replacing `javafx-controls` or standard JavaFX themes
It's not a goal to re-invent any existing `javafx-controls` component or replace standard JavaFX themes. Libraries come and gone, but committing into the core project benefits all the community.
- Mobile support
This is a tremendous amount of work. Just use [Gluon Mobile](https://gluonhq.com/products/mobile/).
- Providing theme API
AtlantaFX provides the Theme interface, which is nothing but a simple wrapper around the stylesheet path. [PR](https://github.com/openjdk/jfx/pull/511) is on its way, let's hope it will ever be merged.

64
base/pom.xml Executable file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-parent</artifactId>
<version>0.1.0</version>
</parent>
<artifactId>atlantafx-base</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>atlantafx-styles</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<!-- place all resources under module package prefix -->
<resource>
<directory>src/main/resources</directory>
<targetPath>atlantafx/base</targetPath>
<filtering>false</filtering>
</resource>
<!-- copy compiled CSS to the classpath -->
<resource>
<directory>../styles/dist</directory>
<targetPath>atlantafx/base/theme</targetPath>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<!-- not used, just prevents installing NodeJS to the module dir -->
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<configuration>
<installDirectory>${project.parent.basedir}</installDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,30 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
public abstract class BehaviorBase<C extends Control, S extends SkinBase<C>> {
private C control;
private S skin;
protected BehaviorBase(C control, S skin) {
this.control = control;
this.skin = skin;
}
public C getControl() {
return control;
}
public S getSkin() {
return skin;
}
/** Called from {@link SkinBase#dispose()} to clean up the behavior state */
public void dispose() {
this.control = null;
this.skin = null;
}
}

@ -0,0 +1,53 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import java.util.function.Consumer;
public abstract class BehaviorSkinBase<C extends Control, B extends BehaviorBase<C, ?>> extends SkinBase<C> {
protected B behavior;
protected BehaviorSkinBase(C control) {
super(control);
behavior = createDefaultBehavior();
}
public abstract B createDefaultBehavior();
public C getControl() {
return getSkinnable();
}
public B getBehavior() {
return behavior;
}
/**
* Unbinds all properties and removes any listeners before disposing the skin.
* There's no need to remove listeners, which has been registered using
* {@link SkinBase#registerChangeListener(ObservableValue, Consumer)} method,
* because it will be done automatically from dispose method.
*/
protected void unregisterListeners() { }
/**
* {@inheritDoc}
*/
@Override
public void dispose() {
unregisterListeners();
// unregister weak listeners and remove reference to the control
super.dispose();
// cleanup the behavior
if (behavior != null) {
behavior.dispose();
behavior = null;
}
}
}

@ -0,0 +1,359 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2014, 2020, ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.util.Callback;
import java.util.UUID;
/**
* Represents a bread crumb bar. This control is useful to visualize and navigate
* a hierarchical path structure, such as file systems.
*/
@SuppressWarnings("unused")
public class Breadcrumbs<T> extends Control {
private static final String STYLE_CLASS_FIRST = "first";
/** Represents an Event which is fired when a bread crumb was activated. */
public static class BreadCrumbActionEvent<TE> extends Event {
/**
* The event type that should be listened to by people interested in
* knowing when the {@link Breadcrumbs#selectedCrumbProperty() selected crumb}
* has changed.
*/
public static final EventType<BreadCrumbActionEvent<?>> CRUMB_ACTION
= new EventType<>("CRUMB_ACTION" + UUID.randomUUID());
private final TreeItem<TE> selectedCrumb;
/** Creates a new event that can subsequently be fired. */
public BreadCrumbActionEvent(TreeItem<TE> selectedCrumb) {
super(CRUMB_ACTION);
this.selectedCrumb = selectedCrumb;
}
/** Returns the crumb which was the action target. */
public TreeItem<TE> getSelectedCrumb() {
return selectedCrumb;
}
}
/**
* Construct a tree model from the flat list which then can be set
* as selectedCrumb node to be shown
*/
@SafeVarargs
public static <T> TreeItem<T> buildTreeModel(T... crumbs) {
TreeItem<T> subRoot = null;
for (T crumb : crumbs) {
TreeItem<T> currentNode = new TreeItem<>(crumb);
if (subRoot != null) {
subRoot.getChildren().add(currentNode);
}
subRoot = currentNode;
}
return subRoot;
}
/** Default crumb node factory. This factory is used when no custom factory is specified by the user. */
private final Callback<TreeItem<T>, Button> defaultCrumbNodeFactory =
crumb -> new BreadCrumbButton(crumb.getValue() != null ? crumb.getValue().toString() : "");
/** Creates an empty bread crumb bar. */
public Breadcrumbs() {
this(null);
}
/**
* Creates a bread crumb bar with the given TreeItem as the currently
* selected crumb.
*/
public Breadcrumbs(TreeItem<T> selectedCrumb) {
getStyleClass().add(DEFAULT_STYLE_CLASS);
setSelectedCrumb(selectedCrumb);
setCrumbFactory(defaultCrumbNodeFactory);
}
/**
* Represents the bottom-most path node (the node on the most-right side in
* terms of the bread crumb bar). The full path is then being constructed
* using getParent() of the tree-items.
*
* <p>
* Consider the following hierarchy:
* [Root] &gt; [Folder] &gt; [SubFolder] &gt; [file.txt]
* <p>
* To show the above bread crumb bar, you have to set the [file.txt] tree-node as selected crumb.
*/
public final ObjectProperty<TreeItem<T>> selectedCrumbProperty() {
return selectedCrumb;
}
private final ObjectProperty<TreeItem<T>> selectedCrumb =
new SimpleObjectProperty<>(this, "selectedCrumb");
/** Get the current target path. */
public final TreeItem<T> getSelectedCrumb() {
return selectedCrumb.get();
}
/** Select one node in the BreadCrumbBar for being the bottom-most path node. */
public final void setSelectedCrumb(TreeItem<T> selectedCrumb) {
this.selectedCrumb.set(selectedCrumb);
}
/**
* Enable or disable auto navigation (default is enabled).
* If auto navigation is enabled, it will automatically navigate to the crumb which was clicked by the user.
*
* @return a {@link BooleanProperty}
*/
public final BooleanProperty autoNavigationEnabledProperty() {
return autoNavigation;
}
private final BooleanProperty autoNavigation =
new SimpleBooleanProperty(this, "autoNavigationEnabled", true);
/**
* Return whether auto-navigation is enabled.
*
* @return whether auto-navigation is enabled.
*/
public final boolean isAutoNavigationEnabled() {
return autoNavigation.get();
}
/**
* Enable or disable auto navigation (default is enabled).
* If auto navigation is enabled, it will automatically navigate to the crumb which was clicked by the user.
*/
public final void setAutoNavigationEnabled(boolean enabled) {
autoNavigation.set(enabled);
}
/**
* Return an ObjectProperty of the CrumbFactory.
*
* @return an ObjectProperty of the CrumbFactory.
*/
public final ObjectProperty<Callback<TreeItem<T>, Button>> crumbFactoryProperty() {
return crumbFactory;
}
private final ObjectProperty<Callback<TreeItem<T>, Button>> crumbFactory =
new SimpleObjectProperty<>(this, "crumbFactory");
/**
* Sets the crumb factory to create (custom) {@link BreadCrumbButton} instances.
* <code>null</code> is not allowed and will result in a fallback to the default factory.
*/
public final void setCrumbFactory(Callback<TreeItem<T>, Button> value) {
if (value == null) {
value = defaultCrumbNodeFactory;
}
crumbFactoryProperty().set(value);
}
/**
* Returns the cell factory that will be used to create {@link BreadCrumbButton}
* instances
*/
public final Callback<TreeItem<T>, Button> getCrumbFactory() {
return crumbFactory.get();
}
/**
* @return an ObjectProperty representing the crumbAction EventHandler being used.
*/
public final ObjectProperty<EventHandler<BreadCrumbActionEvent<T>>> onCrumbActionProperty() {
return onCrumbAction;
}
/** Set a new EventHandler for when a user selects a crumb. */
public final void setOnCrumbAction(EventHandler<BreadCrumbActionEvent<T>> value) {
onCrumbActionProperty().set(value);
}
/**
* Return the EventHandler currently used when a user selects a crumb.
*
* @return the EventHandler currently used when a user selects a crumb.
*/
public final EventHandler<BreadCrumbActionEvent<T>> getOnCrumbAction() {
return onCrumbActionProperty().get();
}
private final ObjectProperty<EventHandler<BreadCrumbActionEvent<T>>> onCrumbAction = new ObjectPropertyBase<>() {
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
protected void invalidated() {
setEventHandler(BreadCrumbActionEvent.CRUMB_ACTION, (EventHandler<BreadCrumbActionEvent>) (Object) get());
}
@Override
public Object getBean() {
return Breadcrumbs.this;
}
@Override
public String getName() {
return "onCrumbAction";
}
};
private static final String DEFAULT_STYLE_CLASS = "bread-crumb-bar";
/** {@inheritDoc} */
@Override
protected Skin<?> createDefaultSkin() {
return new BreadcrumbsSkin<>(this);
}
@SuppressWarnings("FieldCanBeLocal")
public static class BreadCrumbButton extends Button {
private final ObjectProperty<Boolean> first = new SimpleObjectProperty<>(this, STYLE_CLASS_FIRST);
private final double arrowWidth = 5;
private final double arrowHeight = 20;
/**
* Create a BreadCrumbButton
*
* @param text Buttons text
*/
public BreadCrumbButton(String text) {
this(text, null);
}
/**
* Create a BreadCrumbButton
*
* @param text Buttons text
* @param gfx Gfx of the Button
*/
public BreadCrumbButton(String text, Node gfx) {
super(text, gfx);
first.set(false);
getStyleClass().addListener((InvalidationListener) obs -> updateShape());
updateShape();
}
private void updateShape() {
this.setShape(createButtonShape());
}
/** Returns the crumb arrow width. */
public double getArrowWidth() {
return arrowWidth;
}
/** Creates an arrow path. */
private Path createButtonShape() {
// build the following shape (or home without left arrow)
// --------
// \ \
// / /
// --------
Path path = new Path();
// begin in the upper left corner
MoveTo e1 = new MoveTo(0, 0);
path.getElements().add(e1);
// draw a horizontal line that defines the width of the shape
HLineTo e2 = new HLineTo();
// bind the width of the shape to the width of the button
e2.xProperty().bind(this.widthProperty().subtract(arrowWidth));
path.getElements().add(e2);
// draw upper part of right arrow
LineTo e3 = new LineTo();
// the x endpoint of this line depends on the x property of line e2
e3.xProperty().bind(e2.xProperty().add(arrowWidth));
e3.setY(arrowHeight / 2.0);
path.getElements().add(e3);
// draw lower part of right arrow
LineTo e4 = new LineTo();
// the x endpoint of this line depends on the x property of line e2
e4.xProperty().bind(e2.xProperty());
e4.setY(arrowHeight);
path.getElements().add(e4);
// draw lower horizontal line
HLineTo e5 = new HLineTo(0);
path.getElements().add(e5);
if (!getStyleClass().contains(STYLE_CLASS_FIRST)) {
// draw lower part of left arrow
// we simply can omit it for the first Button
LineTo e6 = new LineTo(arrowWidth, arrowHeight / 2.0);
path.getElements().add(e6);
} else {
// draw an arc for the first bread crumb
ArcTo arcTo = new ArcTo();
arcTo.setSweepFlag(true);
arcTo.setX(0);
arcTo.setY(0);
arcTo.setRadiusX(15.0f);
arcTo.setRadiusY(15.0f);
path.getElements().add(arcTo);
}
// close path
ClosePath e7 = new ClosePath();
path.getElements().add(e7);
// this is a dummy color to fill the shape, it won't be visible
path.setFill(Color.BLACK);
return path;
}
}
}

@ -0,0 +1,168 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2014, 2021, ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.beans.value.ChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.SkinBase;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeItem.TreeModificationEvent;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BreadcrumbsSkin<T> extends SkinBase<Breadcrumbs<T>> {
private static final String STYLE_CLASS_FIRST = "first";
public BreadcrumbsSkin(final Breadcrumbs<T> control) {
super(control);
control.selectedCrumbProperty().addListener(selectedPathChangeListener);
updateSelectedPath(getSkinnable().selectedCrumbProperty().get(), null);
}
@SuppressWarnings("FieldCanBeLocal")
private final ChangeListener<TreeItem<T>> selectedPathChangeListener =
(obs, oldItem, newItem) -> updateSelectedPath(newItem, oldItem);
private void updateSelectedPath(TreeItem<T> newTarget, TreeItem<T> oldTarget) {
if (oldTarget != null) {
// remove old listener
oldTarget.removeEventHandler(TreeItem.childrenModificationEvent(), treeChildrenModifiedHandler);
}
if (newTarget != null) {
// add new listener
newTarget.addEventHandler(TreeItem.childrenModificationEvent(), treeChildrenModifiedHandler);
}
updateBreadCrumbs();
}
private final EventHandler<TreeModificationEvent<Object>> treeChildrenModifiedHandler =
args -> updateBreadCrumbs();
private void updateBreadCrumbs() {
final Breadcrumbs<T> buttonBar = getSkinnable();
final TreeItem<T> pathTarget = buttonBar.getSelectedCrumb();
final Callback<TreeItem<T>, Button> factory = buttonBar.getCrumbFactory();
getChildren().clear();
if (pathTarget != null) {
List<TreeItem<T>> crumbs = constructFlatPath(pathTarget);
for (int i = 0; i < crumbs.size(); i++) {
Button crumb = createCrumb(factory, crumbs.get(i));
crumb.setMnemonicParsing(false);
if (i == 0) {
if (!crumb.getStyleClass().contains(STYLE_CLASS_FIRST)) {
crumb.getStyleClass().add(STYLE_CLASS_FIRST);
}
} else {
crumb.getStyleClass().remove(STYLE_CLASS_FIRST);
}
getChildren().add(crumb);
}
}
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
for (int i = 0; i < getChildren().size(); i++) {
Node n = getChildren().get(i);
double nw = snapSizeX(n.prefWidth(h));
double nh = snapSizeY(n.prefHeight(-1));
if (i > 0) {
// we have to position the bread crumbs slightly overlapping
double ins = n instanceof Breadcrumbs.BreadCrumbButton ? ((Breadcrumbs.BreadCrumbButton) n).getArrowWidth() : 0;
x = snapPositionX(x - ins);
}
n.resize(nw, nh);
n.relocate(x, y);
x += nw;
}
}
/**
* Construct a flat list for the crumbs
*
* @param bottomMost The crumb node at the end of the path
*/
private List<TreeItem<T>> constructFlatPath(TreeItem<T> bottomMost) {
List<TreeItem<T>> path = new ArrayList<>();
TreeItem<T> current = bottomMost;
do {
path.add(current);
current = current.getParent();
} while (current != null);
Collections.reverse(path);
return path;
}
private Button createCrumb(
final Callback<TreeItem<T>, Button> factory,
final TreeItem<T> selectedCrumb) {
Button crumb = factory.call(selectedCrumb);
crumb.getStyleClass().add("crumb"); //$NON-NLS-1$
// listen to the action event of each bread crumb
crumb.setOnAction(ae -> onBreadCrumbAction(selectedCrumb));
return crumb;
}
/**
* Occurs when a bread crumb gets the action event
*
* @param crumbModel The crumb which received the action event
*/
protected void onBreadCrumbAction(final TreeItem<T> crumbModel) {
final Breadcrumbs<T> breadCrumbBar = getSkinnable();
// fire the composite event in the breadCrumbBar
Event.fireEvent(breadCrumbBar, new Breadcrumbs.BreadCrumbActionEvent<>(crumbModel));
// navigate to the clicked crumb
if (breadCrumbBar.isAutoNavigationEnabled()) {
breadCrumbBar.setSelectedCrumb(crumbModel);
}
}
}

@ -0,0 +1,38 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import atlantafx.base.theme.Styles;
import javafx.beans.property.StringProperty;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
@SuppressWarnings("unused")
public class CaptionMenuItem extends CustomMenuItem {
private final Label title = new Label();
public CaptionMenuItem() {
this(null);
}
public CaptionMenuItem(String text) {
super();
setTitle(text);
setContent(title);
setHideOnClick(false);
getStyleClass().add(Styles.TEXT_CAPTION);
}
public String getTitle() {
return title.getText();
}
public void setTitle(String text) {
title.setText(text);
}
public StringProperty titleProperty() {
return title.textProperty();
}
}

@ -0,0 +1,124 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2013, 2015, ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
/**
* A base class for people wanting to customize a {@link TextField} to contain nodes
* inside the text field itself, without being on top of the users typed-in text.
*/
@SuppressWarnings("unused")
public class CustomTextField extends TextField {
/** Instantiates a default CustomTextField. */
public CustomTextField() {
getStyleClass().add("custom-text-field");
}
public CustomTextField(String text) {
this();
setText(text);
}
///////////////////////////////////////////////////////////////////////////
// Properties //
///////////////////////////////////////////////////////////////////////////
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left");
/**
* @return An ObjectProperty wrapping the {@link Node} that is placed
* on the left of the text field.
*/
public final ObjectProperty<Node> leftProperty() {
return left;
}
/**
* @return the {@link Node} that is placed on the left of the text field.
*/
public final Node getLeft() {
return left.get();
}
/**
* Sets the {@link Node} that is placed on the left of the text field.
*/
public final void setLeft(Node value) {
left.set(value);
}
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right");
/**
* Property representing the {@link Node} that is placed on the right of the text field.
*/
public final ObjectProperty<Node> rightProperty() {
return right;
}
/**
* @return The {@link Node} that is placed on the right of the text field.
*/
public final Node getRight() {
return right.get();
}
/**
* Sets the {@link Node} that is placed on the right of the text field.
*/
public final void setRight(Node value) {
right.set(value);
}
///////////////////////////////////////////////////////////////////////////
// Methods //
///////////////////////////////////////////////////////////////////////////
/** {@inheritDoc} */
@Override
protected Skin<?> createDefaultSkin() {
return new CustomTextFieldSkin(this) {
@Override
public ObjectProperty<Node> leftProperty() {
return CustomTextField.this.leftProperty();
}
@Override
public ObjectProperty<Node> rightProperty() {
return CustomTextField.this.rightProperty();
}
};
}
}

@ -0,0 +1,173 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2013, 2019 ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.beans.property.ObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.layout.StackPane;
import javafx.scene.text.HitInfo;
public abstract class CustomTextFieldSkin extends TextFieldSkin {
private static final PseudoClass HAS_NO_SIDE_NODE = PseudoClass.getPseudoClass("no-side-nodes");
private static final PseudoClass HAS_LEFT_NODE = PseudoClass.getPseudoClass("left-node-visible");
private static final PseudoClass HAS_RIGHT_NODE = PseudoClass.getPseudoClass("right-node-visible");
private StackPane leftPane;
private StackPane rightPane;
private final TextField control;
public CustomTextFieldSkin(final TextField control) {
super(control);
this.control = control;
updateChildren();
registerChangeListener(leftProperty(), e -> updateChildren());
registerChangeListener(rightProperty(), e -> updateChildren());
}
public abstract ObjectProperty<Node> leftProperty();
public abstract ObjectProperty<Node> rightProperty();
private void updateChildren() {
Node newLeft = leftProperty().get();
// remove leftPane in any case
getChildren().remove(leftPane);
Node left;
if (newLeft != null) {
leftPane = new StackPane(newLeft);
leftPane.setManaged(false);
leftPane.setAlignment(Pos.CENTER_LEFT);
leftPane.getStyleClass().add("left-pane");
getChildren().add(leftPane);
left = newLeft;
} else {
leftPane = null;
left = null;
}
Node newRight = rightProperty().get();
// remove rightPane in any case
getChildren().remove(rightPane);
Node right;
if (newRight != null) {
rightPane = new StackPane(newRight);
rightPane.setManaged(false);
rightPane.setAlignment(Pos.CENTER_RIGHT);
rightPane.getStyleClass().add("right-pane");
getChildren().add(rightPane);
right = newRight;
} else {
rightPane = null;
right = null;
}
control.pseudoClassStateChanged(HAS_LEFT_NODE, left != null);
control.pseudoClassStateChanged(HAS_RIGHT_NODE, right != null);
control.pseudoClassStateChanged(HAS_NO_SIDE_NODE, left == null && right == null);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
final double fullHeight = h + snappedTopInset() + snappedBottomInset();
final double leftWidth = leftPane == null ? 0.0 : snapSizeX(leftPane.prefWidth(fullHeight));
final double rightWidth = rightPane == null ? 0.0 : snapSizeX(rightPane.prefWidth(fullHeight));
final double textFieldStartX = snapPositionX(x) + snapSizeX(leftWidth);
final double textFieldWidth = w - snapSizeX(leftWidth) - snapSizeX(rightWidth);
super.layoutChildren(textFieldStartX, 0, textFieldWidth, fullHeight);
if (leftPane != null) {
final double leftStartX = 0;
leftPane.resizeRelocate(leftStartX, 0, leftWidth, fullHeight);
}
if (rightPane != null) {
final double rightStartX = w - rightWidth + snappedLeftInset();
rightPane.resizeRelocate(rightStartX, 0, rightWidth, fullHeight);
}
}
@Override
public HitInfo getIndex(double x, double y) {
// This resolves an issue when we have a left Node and the click point is badly returned
// because we weren't considering the shift induced by the leftPane.
final double leftWidth = leftPane == null ? 0.0 : snapSizeX(leftPane.prefWidth(getSkinnable().getHeight()));
return super.getIndex(x - leftWidth, y);
}
@Override
protected double computePrefWidth(double h, double topInset, double rightInset, double bottomInset,
double leftInset) {
final double pw = super.computePrefWidth(h, topInset, rightInset, bottomInset, leftInset);
final double leftWidth = leftPane == null ? 0.0 : snapSizeX(leftPane.prefWidth(h));
final double rightWidth = rightPane == null ? 0.0 : snapSizeX(rightPane.prefWidth(h));
return pw + leftWidth + rightWidth;
}
@Override
protected double computePrefHeight(double w, double topInset, double rightInset, double bottomInset,
double leftInset) {
final double ph = super.computePrefHeight(w, topInset, rightInset, bottomInset, leftInset);
final double leftHeight = leftPane == null ? 0.0 : snapSizeX(leftPane.prefHeight(-1));
final double rightHeight = rightPane == null ? 0.0 : snapSizeX(rightPane.prefHeight(-1));
return Math.max(ph, Math.max(leftHeight, rightHeight));
}
@Override
protected double computeMinWidth(double h, double topInset, double rightInset, double bottomInset,
double leftInset) {
final double mw = super.computeMinWidth(h, topInset, rightInset, bottomInset, leftInset);
final double leftWidth = leftPane == null ? 0.0 : snapSizeX(leftPane.minWidth(h));
final double rightWidth = rightPane == null ? 0.0 : snapSizeX(rightPane.minWidth(h));
return mw + leftWidth + rightWidth;
}
@Override
protected double computeMinHeight(double w, double topInset, double rightInset, double bottomInset,
double leftInset) {
final double mh = super.computeMinHeight(w, topInset, rightInset, bottomInset, leftInset);
final double leftHeight = leftPane == null ? 0.0 : snapSizeX(leftPane.minHeight(-1));
final double rightHeight = rightPane == null ? 0.0 : snapSizeX(rightPane.minHeight(-1));
return Math.max(mh, Math.max(leftHeight, rightHeight));
}
}

@ -0,0 +1,295 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2013, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package atlantafx.base.controls;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.WritableValue;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.BooleanConverter;
import javafx.scene.control.*;
import javafx.util.Callback;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.chrono.Chronology;
import java.time.chrono.IsoChronology;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* The DatePicker control allows the user to select a date. The calendar is based on either
* the standard ISO-8601 chronology or any of the other chronology classes defined in the
* java.time.chrono package.
* <p>
* The {@link #valueProperty() value} property represents the currently selected
* {@link LocalDate}. The default value is null.
* <p>
* The {@link #chronologyProperty() chronology} property specifies a calendar system to be used
* for parsing, displaying, and choosing dates.
* <p>
* The {@link #valueProperty() value} property is always defined in the ISO calendar system,
* however, so applications based on a different chronology may use the conversion methods
* provided in the {@link java.time.chrono.Chronology} API to get or set the corresponding
* {@link java.time.chrono.ChronoLocalDate} value.
*/
public class InlineDatePicker extends Control {
protected LocalDate lastValidDate = null;
protected Chronology lastValidChronology = IsoChronology.INSTANCE;
/** Creates a default DatePicker instance with a <code>null</code> date value set. */
public InlineDatePicker() {
this(null);
valueProperty().addListener(obs -> {
LocalDate date = getValue();
Chronology chrono = getChronology();
if (isValidDate(chrono, date)) {
lastValidDate = date;
} else {
System.err.println("Restoring value to " + (lastValidDate == null ? "null" : lastValidDate));
setValue(lastValidDate);
}
});
chronologyProperty().addListener(observable -> {
LocalDate date = getValue();
Chronology chrono = getChronology();
if (isValidDate(chrono, date)) {
lastValidChronology = chrono;
} else {
System.err.println("Restoring value to " + lastValidChronology);
setChronology(lastValidChronology);
}
});
}
/**
* Creates a DatePicker instance and sets the {@link #valueProperty() value} to the given date.
*
* @param localDate to be set as the currently selected date in the DatePicker. Can be null.
*/
public InlineDatePicker(LocalDate localDate) {
setValue(localDate);
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/** {@inheritDoc} */
@Override
protected Skin<?> createDefaultSkin() {
return new InlineDatePickerSkin(this);
}
///////////////////////////////////////////////////////////////////////////
// Properties //
///////////////////////////////////////////////////////////////////////////
private final ObjectProperty<LocalDate> value = new SimpleObjectProperty<>(this, "value");
public final LocalDate getValue() {
return valueProperty().get();
}
public final void setValue(LocalDate value) {
valueProperty().set(value);
}
public ObjectProperty<LocalDate> valueProperty() { return value; }
/**
* A custom cell factory can be provided to customize individual day cells
* Refer to {@link DateCell} and {@link Cell} for more information on cell factories.
*/
private ObjectProperty<Callback<InlineDatePicker, DateCell>> dayCellFactory;
public final void setDayCellFactory(Callback<InlineDatePicker, DateCell> value) {
dayCellFactoryProperty().set(value);
}
public final Callback<InlineDatePicker, DateCell> getDayCellFactory() {
return (dayCellFactory != null) ? dayCellFactory.get() : null;
}
public final ObjectProperty<Callback<InlineDatePicker, DateCell>> dayCellFactoryProperty() {
if (dayCellFactory == null) {
dayCellFactory = new SimpleObjectProperty<>(this, "dayCellFactory");
}
return dayCellFactory;
}
/**
* The calendar system used for parsing, displaying, and choosing dates in the DatePicker
* control.
* <p>
* The default is usually {@link IsoChronology} unless provided explicitly
* in the {@link Locale} by use of a Locale calendar extension.
* <p>
* Setting the value to <code>null</code> will restore the default chronology.
*
* @return a property representing the Chronology being used
*/
public ObjectProperty<Chronology> chronologyProperty() {
return chronology;
}
private final ObjectProperty<Chronology> chronology = new SimpleObjectProperty<>(this, "chronology", null);
public final Chronology getChronology() {
Chronology chrono = chronology.get();
if (chrono == null) {
try {
chrono = Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT));
} catch (Exception e) {
e.printStackTrace();
}
if (chrono == null) {
chrono = IsoChronology.INSTANCE;
}
}
return chrono;
}
public final void setChronology(Chronology value) {
chronology.setValue(value);
}
/**
* Whether the DatePicker popup should display a column showing week numbers.
* <p>
* The default value is specified in a resource bundle, and depends on the country of the
* current locale.
*
* @return true if popup should display a column showing week numbers
*/
public final BooleanProperty showWeekNumbersProperty() {
if (showWeekNumbers == null) {
showWeekNumbers = new StyleableBooleanProperty(false) {
@Override
public CssMetaData<InlineDatePicker, Boolean> getCssMetaData() {
return StyleableProperties.SHOW_WEEK_NUMBERS;
}
@Override
public Object getBean() {
return InlineDatePicker.this;
}
@Override
public String getName() {
return "showWeekNumbers";
}
};
}
return showWeekNumbers;
}
private BooleanProperty showWeekNumbers;
public final void setShowWeekNumbers(boolean value) {
showWeekNumbersProperty().setValue(value);
}
public final boolean isShowWeekNumbers() {
return showWeekNumbersProperty().getValue();
}
///////////////////////////////////////////////////////////////////////////
// Stylesheet Handling //
///////////////////////////////////////////////////////////////////////////
private static final String DEFAULT_STYLE_CLASS = "inline-date-picker";
private static class StyleableProperties {
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
private static final CssMetaData<InlineDatePicker, Boolean> SHOW_WEEK_NUMBERS =
new CssMetaData<>("-fx-show-week-numbers", BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(InlineDatePicker n) {
return n.showWeekNumbers == null || !n.showWeekNumbers.isBound();
}
@Override
@SuppressWarnings("RedundantCast")
public StyleableProperty<Boolean> getStyleableProperty(InlineDatePicker n) {
return (StyleableProperty<Boolean>) (WritableValue<Boolean>) n.showWeekNumbersProperty();
}
};
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
Collections.addAll(styleables, SHOW_WEEK_NUMBERS);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its superclasses.
*/
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/** {@inheritDoc} */
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
static boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) {
if (date != null) {
try {
return isValidDate(chrono, date.plus(offset, unit));
} catch (DateTimeException e) {
e.printStackTrace();
}
}
return false;
}
static boolean isValidDate(Chronology chrono, LocalDate date) {
try {
if (date != null) { chrono.date(date); }
return true;
} catch (DateTimeException e) {
e.printStackTrace();
return false;
}
}
}

@ -0,0 +1,86 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import java.time.LocalDate;
import static atlantafx.base.util.PlatformUtils.isMac;
import static java.time.temporal.ChronoUnit.MONTHS;
import static java.time.temporal.ChronoUnit.YEARS;
import static javafx.scene.input.KeyCode.ESCAPE;
public class InlineDatePickerBehavior extends BehaviorBase<InlineDatePicker, InlineDatePickerSkin> {
public InlineDatePickerBehavior(InlineDatePicker control, InlineDatePickerSkin skin) {
super(control, skin);
}
public void onKeyPressed(KeyEvent e) {
getSkin().rememberFocusedDayCell();
if (e.getEventType() == KeyEvent.KEY_PRESSED) {
switch (e.getCode()) {
case HOME -> {
getSkin().goToDate(LocalDate.now(), true);
e.consume();
}
case PAGE_UP -> {
if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) {
if (getSkin().canGoYearForward()) {
getSkin().forward(1, YEARS, true);
}
} else {
if (getSkin().canGoMonthForward()) {
getSkin().forward(1, MONTHS, true);
}
}
e.consume();
}
case PAGE_DOWN -> {
if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) {
if (getSkin().canGoYearBack()) {
getSkin().forward(-1, YEARS, true);
}
} else {
if (getSkin().canGoMonthBack()) {
getSkin().forward(-1, MONTHS, true);
}
}
e.consume();
}
}
getSkin().rememberFocusedDayCell();
}
// prevents any other key events but ESC from reaching the control owner
if (e.getCode() != ESCAPE) { e.consume(); }
}
public void moveForward(MouseEvent e) {
if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) {
if (getSkin().canGoYearForward()) {
getSkin().forward(1, YEARS, true);
}
} else {
if (getSkin().canGoMonthForward()) {
getSkin().forward(1, MONTHS, true);
}
}
e.consume();
}
public void moveBackward(MouseEvent e) {
if ((isMac() && e.isMetaDown()) || (!isMac() && e.isControlDown())) {
if (getSkin().canGoYearBack()) {
getSkin().forward(-1, YEARS, true);
}
} else {
if (getSkin().canGoMonthBack()) {
getSkin().forward(-1, MONTHS, true);
}
}
e.consume();
}
}

@ -0,0 +1,585 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package atlantafx.base.controls;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.DateCell;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.util.Callback;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.Chronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DecimalStyle;
import java.time.temporal.ChronoUnit;
import java.time.temporal.ValueRange;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import static atlantafx.base.controls.InlineDatePicker.isValidDate;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoUnit.*;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, InlineDatePickerBehavior> {
// formatters
final DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("y");
final DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MMMM");
final DateTimeFormatter weekNumberFormatter = DateTimeFormatter.ofPattern("w");
final DateTimeFormatter dayCellFormatter = DateTimeFormatter.ofPattern("d");
final DateTimeFormatter monthFormatterSO = DateTimeFormatter.ofPattern("LLLL"); // standalone month name
final DateTimeFormatter weekDayNameFormatter = DateTimeFormatter.ofPattern("ccc"); // standalone day name
// UI
protected final VBox rootPane = new VBox();
protected CalendarGrid calendarGrid;
protected Button forwardButton;
protected Button backButton;
protected Label monthLabel;
protected Label yearLabel;
// model
protected final List<DateCell> dayNameCells = new ArrayList<>();
protected final List<DateCell> weekNumberCells = new ArrayList<>();
protected final List<DateCell> dayCells = new ArrayList<>();
protected LocalDate[] dayCellDates;
protected DateCell lastFocusedDayCell = null;
protected final int daysPerWeek = getDaysPerWeek();
private final ObjectProperty<YearMonth> displayedYearMonth = new SimpleObjectProperty<>(this, "displayedYearMonth");
public ObjectProperty<YearMonth> displayedYearMonthProperty() { return displayedYearMonth; }
private final ObjectBinding<LocalDate> firstDayOfMonth = Bindings.createObjectBinding(() -> displayedYearMonth.get().atDay(1), displayedYearMonth);
public LocalDate getFirstDayOfMonth() { return firstDayOfMonth.get(); }
public InlineDatePickerSkin(InlineDatePicker datePicker) {
super(datePicker);
createUI();
registerChangeListener(datePicker.valueProperty(), e -> {
LocalDate date = datePicker.getValue();
displayedYearMonthProperty().set((date != null) ? YearMonth.from(date) : YearMonth.now());
updateValues();
datePicker.fireEvent(new ActionEvent());
});
registerChangeListener(datePicker.showWeekNumbersProperty(), e -> {
updateGrid();
updateWeekNumberCells();
});
}
@Override
public InlineDatePickerBehavior createDefaultBehavior() {
return new InlineDatePickerBehavior(getControl(), this);
}
public Locale getLocale() {
return Locale.getDefault(Locale.Category.FORMAT);
}
public Scene getScene() {
return getControl().getScene();
}
/**
* The primary chronology for display. This may be overridden to be different from the
* DatePicker chronology. For example DatePickerHijrahContent uses ISO as primary and Hijrah
* as a secondary chronology.
*/
public Chronology getPrimaryChronology() {
return getControl().getChronology();
}
public int getMonthsPerYear() {
ValueRange range = getPrimaryChronology().range(MONTH_OF_YEAR);
return (int) (range.getMaximum() - range.getMinimum() + 1);
}
public int getDaysPerWeek() {
ValueRange range = getPrimaryChronology().range(DAY_OF_WEEK);
return (int) (range.getMaximum() - range.getMinimum() + 1);
}
///////////////////////////////////////////////////////////////////////////
// UI //
///////////////////////////////////////////////////////////////////////////
protected void createUI() {
// YearMonth //
LocalDate value = getControl().getValue();
displayedYearMonth.set(value != null ? YearMonth.from(value) : YearMonth.now());
displayedYearMonth.addListener((observable, oldValue, newValue) -> updateValues());
rootPane.getChildren().add(createMonthYearPane());
// Calendar //
calendarGrid = new CalendarGrid();
calendarGrid.getStyleClass().add("calendar-grid");
calendarGrid.setFocusTraversable(true);
calendarGrid.setVgap(-1);
calendarGrid.setHgap(-1);
// get the weekday labels starting with the weekday that is the first-day-of-the-week
// according to the locale in the displayed LocalDate
for (int i = 0; i < daysPerWeek; i++) {
DateCell cell = new DateCell();
cell.getStyleClass().add("day-name-cell");
dayNameCells.add(cell);
}
// week number column
for (int i = 0; i < 6; i++) {
DateCell cell = new DateCell();
cell.getStyleClass().add("week-number-cell");
weekNumberCells.add(cell);
}
createDayCells();
updateGrid();
// preserve default class name for compatibility reasons
rootPane.getStyleClass().addAll("date-picker-popup", "inline-date-picker");
rootPane.getChildren().add(calendarGrid);
getChildren().add(rootPane);
getControl().setOnKeyPressed(e -> behavior.onKeyPressed(e));
refresh();
}
protected HBox createMonthYearPane() {
HBox monthYearPane = new HBox();
monthYearPane.getStyleClass().add("month-year-pane");
backButton = new Button();
backButton.getStyleClass().addAll("back-button");
backButton.setOnMouseClicked(e -> behavior.moveBackward(e));
StackPane leftArrow = new StackPane();
leftArrow.getStyleClass().add("left-arrow");
leftArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
backButton.setGraphic(leftArrow);
Region leftSpacer = new Region();
HBox.setHgrow(leftSpacer, Priority.ALWAYS);
monthLabel = new Label();
monthLabel.getStyleClass().add("month-label");
yearLabel = new Label();
yearLabel.getStyleClass().add("year-label");
Region rightSpacer = new Region();
HBox.setHgrow(rightSpacer, Priority.ALWAYS);
forwardButton = new Button();
forwardButton.getStyleClass().addAll("forward-button");
forwardButton.setOnMouseClicked(e -> behavior.moveForward(e));
StackPane rightArrow = new StackPane();
rightArrow.getStyleClass().add("right-arrow");
rightArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
forwardButton.setGraphic(rightArrow);
monthYearPane.getChildren().addAll(backButton, leftSpacer, monthLabel, yearLabel, rightSpacer, forwardButton);
return monthYearPane;
}
protected class CalendarGrid extends GridPane {
@Override
protected double computePrefWidth(double height) {
final double width = super.computePrefWidth(height);
// Make sure width snaps to pixel when divided by number of columns.
// GridPane doesn't do this with percentage width constraints.
// See GridPane.adjustColumnWidths().
final int nCols = daysPerWeek + (getControl().isShowWeekNumbers() ? 1 : 0);
final double snapHGap = snapSpaceX(getHgap());
final double hGaps = snapHGap * (nCols - 1);
final double left = snapSpaceX(getInsets().getLeft());
final double right = snapSpaceX(getInsets().getRight());
final double contentWidth = width - left - right - hGaps;
return ((snapSizeX(contentWidth / nCols)) * nCols) + left + right + hGaps;
}
@Override
protected void layoutChildren() {
// prevent AssertionError in GridPane
if (getWidth() > 0 && getHeight() > 0) {
super.layoutChildren();
}
}
}
///////////////////////////////////////////////////////////////////////////
// API //
///////////////////////////////////////////////////////////////////////////
public void refresh() {
updateDayNameCells();
updateValues();
}
public void updateValues() {
// preserve this order
updateWeekNumberCells();
updateDayCells();
updateMonthYearPane();
}
public void updateGrid() {
calendarGrid.getColumnConstraints().clear();
calendarGrid.getChildren().clear();
final int nCols = daysPerWeek + (getControl().isShowWeekNumbers() ? 1 : 0);
// column constraints
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(100); // treated as weight
for (int i = 0; i < nCols; i++) {
calendarGrid.getColumnConstraints().add(columnConstraints);
}
// day names row
for (int i = 0; i < daysPerWeek; i++) {
calendarGrid.add(dayNameCells.get(i), i + nCols - daysPerWeek, 1);
}
// week number column
if (getControl().isShowWeekNumbers()) {
for (int i = 0; i < 6; i++) {
calendarGrid.add(weekNumberCells.get(i), 0, i + 2);
}
}
// setup 6 rows of daysPerWeek, which is the maximum number of cells
// required in the worst case layout
for (int row = 0; row < 6; row++) {
for (int col = 0; col < daysPerWeek; col++) {
calendarGrid.add(dayCells.get(row * daysPerWeek + col), col + nCols - daysPerWeek, row + 2);
}
}
}
public void updateDayNameCells() {
// first day of week, 1 = monday, 7 = sunday
final int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue();
// july 13th 2009 is a Monday, so a firstDayOfWeek = 1 must come out of the 13th
final LocalDate date = LocalDate.of(2009, 7, 12 + firstDayOfWeek);
for (int i = 0; i < daysPerWeek; i++) {
String name = weekDayNameFormatter.withLocale(getLocale()).format(date.plus(i, DAYS));
dayNameCells.get(i).setText(capitalize(name));
}
}
public void updateWeekNumberCells() {
if (getControl().isShowWeekNumbers()) {
final Locale locale = getLocale();
final int maxWeeksPerMonth = 6;
final LocalDate firstOfMonth = displayedYearMonth.get().atDay(1);
for (int i = 0; i < maxWeeksPerMonth; i++) {
LocalDate date = firstOfMonth.plus(i, WEEKS);
// use a formatter to ensure correct localization
// such as when Thai numerals are required.
String cellText = weekNumberFormatter
.withLocale(locale)
.withDecimalStyle(DecimalStyle.of(locale))
.format(date);
weekNumberCells.get(i).setText(cellText);
}
}
}
public void updateDayCells() {
final Locale locale = getLocale();
final Chronology chrono = getPrimaryChronology();
final YearMonth curMonth = displayedYearMonth.get();
final int firstOfMonthIdx = determineFirstOfMonthDayOfWeek();
YearMonth prevMonth = null;
YearMonth nextMonth = null;
int daysInCurMonth = -1;
int daysInPrevMonth = -1;
for (int i = 0; i < 6 * daysPerWeek; i++) {
final DateCell dayCell = dayCells.get(i);
dayCell.getStyleClass().setAll("cell", "date-cell", "day-cell");
dayCell.setDisable(false);
dayCell.setStyle(null);
dayCell.setGraphic(null);
dayCell.setTooltip(null);
try {
daysInCurMonth = daysInCurMonth == -1 ? curMonth.lengthOfMonth() : daysInCurMonth;
YearMonth month = curMonth;
int day = i - firstOfMonthIdx + 1;
if (i < firstOfMonthIdx) {
if (prevMonth == null) {
prevMonth = curMonth.minusMonths(1);
daysInPrevMonth = prevMonth.lengthOfMonth();
}
month = prevMonth;
day = i + daysInPrevMonth - firstOfMonthIdx + 1;
dayCell.getStyleClass().add("previous-month");
} else if (i >= firstOfMonthIdx + daysInCurMonth) {
if (nextMonth == null) {
nextMonth = curMonth.plusMonths(1);
}
month = nextMonth;
day = i - daysInCurMonth - firstOfMonthIdx + 1;
dayCell.getStyleClass().add("next-month");
}
LocalDate date = month.atDay(day);
dayCellDates[i] = date;
ChronoLocalDate cDate = chrono.date(date);
dayCell.setDisable(false);
if (isToday(date)) {
dayCell.getStyleClass().add("today");
}
if (date.equals(getControl().getValue())) {
dayCell.getStyleClass().add("selected");
}
String cellText = dayCellFormatter.withLocale(locale)
.withChronology(chrono)
.withDecimalStyle(DecimalStyle.of(locale))
.format(cDate);
dayCell.setText(cellText);
dayCell.updateItem(date, false);
} catch (DateTimeException ex) {
// date is out of range
dayCell.setText(" ");
dayCell.setDisable(true);
}
}
}
// determine on which day of week idx the first of the months is
private int determineFirstOfMonthDayOfWeek() {
// determine with which cell to start
int firstDayOfWeek = WeekFields.of(getLocale()).getFirstDayOfWeek().getValue();
int firstOfMonthIdx = displayedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek;
return firstOfMonthIdx < 0 ? firstOfMonthIdx + daysPerWeek : firstOfMonthIdx;
}
public void updateMonthYearPane() {
YearMonth yearMonth = displayedYearMonth.get();
monthLabel.setText(formatMonth(yearMonth));
yearLabel.setText(formatYear(yearMonth));
backButton.setDisable(!canGoMonthBack());
forwardButton.setDisable(!canGoMonthForward());
}
protected String formatMonth(YearMonth yearMonth) {
Chronology chrono = getPrimaryChronology();
try {
ChronoLocalDate chronoDate = chrono.date(yearMonth.atDay(1));
String str = monthFormatterSO.withLocale(getLocale())
.withChronology(chrono)
.format(chronoDate);
if (Character.isDigit(str.charAt(0))) {
// fallback: if standalone format returned a number, use standard format instead
str = monthFormatter.withLocale(getLocale())
.withChronology(chrono)
.format(chronoDate);
}
return capitalize(str);
} catch (DateTimeException ex) {
// date is out of range
return "";
}
}
protected String formatYear(YearMonth yearMonth) {
Chronology chrono = getPrimaryChronology();
try {
ChronoLocalDate chronoDate = chrono.date(yearMonth.atDay(1));
return yearFormatter.withLocale(getLocale())
.withChronology(chrono)
.withDecimalStyle(DecimalStyle.of(getLocale()))
.format(chronoDate);
} catch (DateTimeException ex) {
// date is out of range
return "";
}
}
public void forward(int offset, ChronoUnit unit, boolean focusDayCell) {
YearMonth yearMonth = displayedYearMonth.get();
DateCell dateCell = lastFocusedDayCell;
if (dateCell == null || !getDayCellDate(dateCell).getMonth().equals(yearMonth.getMonth())) {
dateCell = findDayCellForDate(yearMonth.atDay(1));
}
goToDayCell(dateCell, offset, unit, focusDayCell);
}
public void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) {
goToDate(getDayCellDate(dateCell).plus(offset, unit), focusDayCell);
}
public void goToDate(LocalDate date, boolean focusDayCell) {
if (isValidDate(getPrimaryChronology(), date)) {
displayedYearMonth.set(YearMonth.from(date));
if (focusDayCell) {
findDayCellForDate(date).requestFocus();
}
}
}
private DateCell findDayCellForDate(LocalDate date) {
for (int i = 0; i < dayCellDates.length; i++) {
if (date.equals(dayCellDates[i])) {
return dayCells.get(i);
}
}
return dayCells.get(dayCells.size() / 2 + 1);
}
public void selectDayCell(DateCell dateCell) {
getControl().setValue(getDayCellDate(dateCell));
}
private LocalDate getDayCellDate(DateCell dateCell) {
return dayCellDates[dayCells.indexOf(dateCell)];
}
protected void createDayCells() {
EventHandler<MouseEvent> dayCellActionHandler = e -> {
if (e.getButton() != MouseButton.PRIMARY) { return; }
DateCell dayCell = (DateCell) e.getSource();
selectDayCell(dayCell);
lastFocusedDayCell = dayCell;
};
for (int row = 0; row < 6; row++) {
for (int col = 0; col < daysPerWeek; col++) {
DateCell dayCell = createDayCell();
dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, dayCellActionHandler);
dayCells.add(dayCell);
}
}
dayCellDates = new LocalDate[6 * daysPerWeek];
}
protected DateCell createDayCell() {
Callback<InlineDatePicker, DateCell> factory = getControl().getDayCellFactory();
return Objects.requireNonNullElseGet(
factory != null ? factory.call(getControl()) : null,
DateCell::new
);
}
public void rememberFocusedDayCell() {
Node node = getControl().getScene().getFocusOwner();
if (node instanceof DateCell) {
lastFocusedDayCell = (DateCell) node;
}
}
public boolean canGoMonthBack() {
return isValidDate(getPrimaryChronology(), getFirstDayOfMonth(), -1, DAYS);
}
public boolean canGoMonthForward() {
return isValidDate(getPrimaryChronology(), getFirstDayOfMonth(), +1, MONTHS);
}
public boolean canGoYearBack() {
return isValidDate(getPrimaryChronology(), getFirstDayOfMonth(), -1, YEARS);
}
public boolean canGoYearForward() {
return isValidDate(getPrimaryChronology(), getFirstDayOfMonth(), +1, YEARS);
}
public void clearFocus() {
LocalDate focusDate = Objects.requireNonNullElseGet(getControl().getValue(), LocalDate::now);
if (YearMonth.from(focusDate).equals(displayedYearMonth.get())) {
goToDate(focusDate, true); // focus date
} else {
backButton.requestFocus(); // should not happen
}
}
private static String capitalize(String word) {
if (word.length() > 0) {
int firstChar = word.codePointAt(0);
if (!Character.isTitleCase(firstChar)) {
word = new String(new int[] {
Character.toTitleCase(firstChar) }, 0, 1) +
word.substring(Character.offsetByCodePoints(word, 0, 1));
}
}
return word;
}
private static boolean isToday(LocalDate date) {
return date != null && date.equals(today());
}
private static LocalDate today() {
return LocalDate.now();
}
}

@ -0,0 +1,920 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2013, 2022 ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Skin;
import javafx.scene.layout.StackPane;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import static java.util.Objects.requireNonNull;
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
/**
* The Popover control provides detailed information about an owning node in a
* popup window. 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 Popover will move around with the parent window when the
* user drags it.
* <p>
* The Popover can be detached from the owning node by dragging it away from the
* owner. It stops displaying an arrow and starts displaying a title and a close
* icon.
*/
@SuppressWarnings("unused")
public class Popover extends PopupControl {
private static final String DEFAULT_STYLE_CLASS = "popover";
private static final Duration DEFAULT_FADE_DURATION = Duration.seconds(.2);
private double targetX;
private double targetY;
private final SimpleBooleanProperty animated = new SimpleBooleanProperty(true);
private final ObjectProperty<Duration> fadeInDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION);
private final ObjectProperty<Duration> fadeOutDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION);
/** Creates a popover with a label as the content node. */
public Popover() {
super();
getStyleClass().add(DEFAULT_STYLE_CLASS);
setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
setOnHiding(evt -> setDetached(false));
/* Create some initial content */
Label label = new Label("No Content");
label.setPrefSize(200, 200);
label.setPadding(new Insets(4));
setContentNode(label);
InvalidationListener repositionListener = observable -> {
if (isShowing() && !isDetached()) {
show(getOwnerNode(), targetX, targetY);
adjustWindowLocation();
}
};
arrowSize.addListener(repositionListener);
cornerRadius.addListener(repositionListener);
arrowLocation.addListener(repositionListener);
arrowIndent.addListener(repositionListener);
headerAlwaysVisible.addListener(repositionListener);
// a detached popover should of course not automatically hide itself
detached.addListener(it -> setAutoHide(!isDetached()));
setAutoHide(true);
}
/**
* Creates a popover with the given node as the content node.
*
* @param content The content shown by the popover
*/
public Popover(Node content) {
this();
setContentNode(content);
}
@Override
protected Skin<?> createDefaultSkin() {
return new PopoverSkin(this);
}
private final StackPane root = new StackPane();
/**
* The root pane stores the content node of the popover. It is accessible
* via this method in order to support proper styling.
*
* Example:
*
* <pre>
* Popover popOver = new Popover();
* popOver.getRoot().getStylesheets().add(...);
* </pre>
*
* @return the root pane
*/
public final StackPane getRoot() {
return root;
}
private final ObjectProperty<Node> contentNode = new SimpleObjectProperty<>(this, "contentNode") {
@Override
public void setValue(Node node) {
if (node == null) { throw new IllegalArgumentException("content node can not be null"); }
}
};
/**
* Returns the content shown by the popover.
*
* @return the content node property
*/
public final ObjectProperty<Node> contentNodeProperty() {
return contentNode;
}
/**
* Returns the value of the content property
*
* @return the content node
*
* @see #contentNodeProperty()
*/
public final Node getContentNode() {
return contentNodeProperty().get();
}
/**
* Sets the value of the content property.
*
* @param content the new content node value
*
* @see #contentNodeProperty()
*/
public final void setContentNode(Node content) {
contentNodeProperty().set(content);
}
private final InvalidationListener hideListener = observable -> {
if (!isDetached()) {
hide(Duration.ZERO);
}
};
private final WeakInvalidationListener weakHideListener = new WeakInvalidationListener(hideListener);
private final ChangeListener<Number> xListener = (value, oldX, newX) -> {
if (!isDetached()) {
setAnchorX(getAnchorX() + (newX.doubleValue() - oldX.doubleValue()));
}
};
private final WeakChangeListener<Number> weakXListener = new WeakChangeListener<>(xListener);
private final ChangeListener<Number> yListener = (value, oldY, newY) -> {
if (!isDetached()) {
setAnchorY(getAnchorY() + (newY.doubleValue() - oldY.doubleValue()));
}
};
private final WeakChangeListener<Number> weakYListener = new WeakChangeListener<>(yListener);
private Window ownerWindow;
private final EventHandler<WindowEvent> closePopoverOnOwnerWindowCloseLambda = event -> ownerWindowHiding();
private final WeakEventHandler<WindowEvent> closePopoverOnOwnerWindowClose =
new WeakEventHandler<>(closePopoverOnOwnerWindowCloseLambda);
/**
* Shows the popover in a position relative to the edges of the given owner
* node. The position is dependent on the arrow location. If the arrow is
* pointing to the right then the popover will be placed to the left of the
* given owner. If the arrow points up then the popover will be placed
* below the given owner node. The arrow will slightly overlap with the
* owner node.
*
* @param owner the owner of the popover
*/
public final void show(Node owner) {
show(owner, 4);
}
/**
* Shows the popover in a position relative to the edges of the given owner
* node. The position is dependent on the arrow location. If the arrow is
* pointing to the right then the popover will be placed to the left of the
* given owner. If the arrow points up then the popover will be placed
* below the given owner node.
*
* @param owner the owner of the popover
* @param offset if negative specifies the distance to the owner node or when
* positive specifies the number of pixels that the arrow will
* overlap with the owner node (positive values are recommended)
*/
public final void show(Node owner, double offset) {
requireNonNull(owner);
Bounds bounds = owner.localToScreen(owner.getBoundsInLocal());
switch (getArrowLocation()) {
case BOTTOM_CENTER, BOTTOM_LEFT, BOTTOM_RIGHT -> show(
owner, bounds.getMinX() + bounds.getWidth() / 2, bounds.getMinY() + offset
);
case LEFT_BOTTOM, LEFT_CENTER, LEFT_TOP -> show(
owner, bounds.getMaxX() - offset, bounds.getMinY() + bounds.getHeight() / 2
);
case RIGHT_BOTTOM, RIGHT_CENTER, RIGHT_TOP -> show(
owner, bounds.getMinX() + offset, bounds.getMinY() + bounds.getHeight() / 2
);
case TOP_CENTER, TOP_LEFT, TOP_RIGHT -> show(
owner, bounds.getMinX() + bounds.getWidth() / 2, bounds.getMinY() + bounds.getHeight() - offset
);
}
}
/** {@inheritDoc} */
@Override
public final void show(Window owner) {
super.show(owner);
ownerWindow = owner;
if (isAnimated()) {
showFadeInAnimation(getFadeInDuration());
}
ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, closePopoverOnOwnerWindowClose);
}
/** {@inheritDoc} */
@Override
public final void show(Window ownerWindow, double anchorX, double anchorY) {
super.show(ownerWindow, anchorX, anchorY);
this.ownerWindow = ownerWindow;
if (isAnimated()) {
showFadeInAnimation(getFadeInDuration());
}
ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, closePopoverOnOwnerWindowClose);
}
/**
* Makes the popover visible at the give location and associates it with
* the given owner node. The x and y coordinate will be the target location
* of the arrow of the popover and not the location of the window.
*
* @param owner the owning node
* @param x the x coordinate for the popover arrow tip
* @param y the y coordinate for the popover arrow tip
*/
@Override
public final void show(Node owner, double x, double y) {
show(owner, x, y, getFadeInDuration());
}
/**
* Makes the popover visible at the give location and associates it with
* the given owner node. The x and y coordinate will be the target location
* of the arrow of the popover and not the location of the window.
*
* @param owner the owning node
* @param x the x coordinate for the popover arrow tip
* @param y the y coordinate for the popover arrow tip
* @param fadeInDuration the time it takes for the popover to be fully visible.
* This duration takes precedence over the fade-in property without setting.
*/
public final void show(Node owner, double x, double y, Duration fadeInDuration) {
/*
* Calling show() a second time without first closing the popover
* causes it to be placed at the wrong location.
*/
if (ownerWindow != null && isShowing()) {
super.hide();
}
targetX = x;
targetY = y;
if (owner == null) {
throw new IllegalArgumentException("owner can not be null");
}
// this is all needed because children windows do not get their x and y
// coordinate updated when the owning window gets moved by the user
if (ownerWindow != null) {
ownerWindow.xProperty().removeListener(weakXListener);
ownerWindow.yProperty().removeListener(weakYListener);
ownerWindow.widthProperty().removeListener(weakHideListener);
ownerWindow.heightProperty().removeListener(weakHideListener);
}
ownerWindow = owner.getScene().getWindow();
ownerWindow.xProperty().addListener(weakXListener);
ownerWindow.yProperty().addListener(weakYListener);
ownerWindow.widthProperty().addListener(weakHideListener);
ownerWindow.heightProperty().addListener(weakHideListener);
setOnShown(evt -> {
// the user clicked somewhere into the transparent background,
// if this is the case then hide the window (when attached)
getScene().addEventHandler(MOUSE_CLICKED, mouseEvent -> {
if (mouseEvent.getTarget().equals(getScene().getRoot())) {
if (!isDetached()) { hide(); }
}
});
// move the window so that the arrow will end up pointing at the target coordinates
adjustWindowLocation();
// Popover flickering fix:
// The reason of flickering is that for calculating popup bounds show() method have to
// be called PRIOR TO adjusting window position. So, in a very short period we see the
// window in its initial position. Ideally, we have to call adjustWindowLocation() right
// after window is added to the scene, but before it's rendered, which is not possible
// due to JavaFX async nature. The only way seems to start popover as invisible (not opaque)
// and then restore its visibility after a fixed delay to hide window repositioning.
// Still it's not a 100% guarantee,but better than nothing.
int delay = Math.min((int) Objects.requireNonNullElse(fadeInDuration, DEFAULT_FADE_DURATION).toMillis() / 2, 250);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Platform.runLater(() -> getSkin().getNode().setVisible(true));
}
}, delay);
});
super.show(owner, x, y);
if (isAnimated()) {
showFadeInAnimation(Objects.requireNonNullElse(fadeInDuration, DEFAULT_FADE_DURATION));
}
ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, closePopoverOnOwnerWindowClose);
}
private void showFadeInAnimation(Duration fadeInDuration) {
// fade in
Node skinNode = getSkin().getNode();
skinNode.setOpacity(0);
FadeTransition fadeIn = new FadeTransition(fadeInDuration, skinNode);
fadeIn.setFromValue(0);
fadeIn.setToValue(1);
fadeIn.play();
}
private void ownerWindowHiding() {
hide(Duration.ZERO);
if (ownerWindow != null) {
// remove EventFilter to prevent memory leak
ownerWindow.removeEventFilter(WindowEvent.WINDOW_HIDING, closePopoverOnOwnerWindowClose);
}
}
/**
* Hides the popover by quickly changing its opacity to 0.
*
* @see #hide(Duration)
*/
@Override
public final void hide() {
hide(getFadeOutDuration());
}
/**
* Hides the popover by quickly changing its opacity to 0.
*
* @param fadeOutDuration the duration of the fade transition that is being used to
* change the opacity of the popover
*/
public final void hide(Duration fadeOutDuration) {
if (fadeOutDuration == null) {
fadeOutDuration = DEFAULT_FADE_DURATION;
}
if (isShowing()) {
if (isAnimated()) {
// fade out
Node skinNode = getSkin().getNode();
FadeTransition fadeOut = new FadeTransition(fadeOutDuration, skinNode);
fadeOut.setFromValue(skinNode.getOpacity());
fadeOut.setToValue(0);
fadeOut.setOnFinished(evt -> super.hide());
fadeOut.play();
} else {
super.hide();
}
getSkin().getNode().setVisible(false);
}
}
private void adjustWindowLocation() {
Bounds bounds = getSkin().getNode().getBoundsInParent();
switch (getArrowLocation()) {
case TOP_CENTER, TOP_LEFT, TOP_RIGHT -> {
setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset());
setAnchorY(getAnchorY() + bounds.getMinY() + getArrowSize());
}
case LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM -> {
setAnchorX(getAnchorX() + bounds.getMinX() + getArrowSize());
setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset());
}
case BOTTOM_CENTER, BOTTOM_LEFT, BOTTOM_RIGHT -> {
setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset());
setAnchorY(getAnchorY() - bounds.getMinY() - bounds.getMaxY() - 1);
}
case RIGHT_TOP, RIGHT_BOTTOM, RIGHT_CENTER -> {
setAnchorX(getAnchorX() - bounds.getMinX() - bounds.getMaxX() - 1);
setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset());
}
}
}
private double computeXOffset() {
return switch (getArrowLocation()) {
case TOP_LEFT, BOTTOM_LEFT -> (
getCornerRadius() + getArrowIndent() + getArrowSize()
);
case TOP_CENTER, BOTTOM_CENTER -> (
getContentNode().prefWidth(-1) / 2
);
case TOP_RIGHT, BOTTOM_RIGHT -> (
getContentNode().prefWidth(-1) - getArrowIndent() - getCornerRadius() - getArrowSize()
);
default -> 0;
};
}
private double computeYOffset() {
double prefContentHeight = getContentNode().prefHeight(-1);
return switch (getArrowLocation()) {
case LEFT_TOP, RIGHT_TOP -> (
getCornerRadius() + getArrowIndent() + getArrowSize()
);
case LEFT_CENTER, RIGHT_CENTER -> (
Math.max(prefContentHeight, 2 * (getCornerRadius() + getArrowIndent() + getArrowSize())) / 2
);
case LEFT_BOTTOM, RIGHT_BOTTOM -> (
Math.max(prefContentHeight - getCornerRadius() - getArrowIndent() - getArrowSize(),
getCornerRadius() + getArrowIndent() + getArrowSize()
)
);
default -> 0;
};
}
/**
* Detaches the popover from the owning node. The popover will no longer
* display an arrow pointing at the owner node.
*/
public final void detach() {
if (isDetachable()) {
setDetached(true);
}
}
private final BooleanProperty headerAlwaysVisible = new SimpleBooleanProperty(this, "headerAlwaysVisible");
/**
* Determines whether the {@link Popover} header should remain visible or not,
* even while attached.
*/
public final BooleanProperty headerAlwaysVisibleProperty() {
return headerAlwaysVisible;
}
/**
* Sets the value of the headerAlwaysVisible property.
*
* @param visible if true, then the header is visible even while attached
*
* @see #headerAlwaysVisibleProperty()
*/
public final void setHeaderAlwaysVisible(boolean visible) {
headerAlwaysVisible.setValue(visible);
}
/**
* Returns the value of the detachable property.
*
* @return true if the header is visible even while attached
*
* @see #headerAlwaysVisibleProperty()
*/
public final boolean isHeaderAlwaysVisible() {
return headerAlwaysVisible.getValue();
}
private final BooleanProperty closeButtonEnabled = new SimpleBooleanProperty(this, "closeButtonEnabled", true);
/**
* Determines whether the header's close button should be available or not.
*/
public final BooleanProperty closeButtonEnabledProperty() {
return closeButtonEnabled;
}
/**
* Sets the value of the closeButtonEnabled property.
*
* @param enabled if false, the popover will not be closeable by the header's close button
*
* @see #closeButtonEnabledProperty()
*/
public final void setCloseButtonEnabled(boolean enabled) {
closeButtonEnabled.setValue(enabled);
}
/**
* Returns the value of the closeButtonEnabled property.
*
* @return true if the header's close button is enabled
*
* @see #closeButtonEnabledProperty()
*/
public final boolean isCloseButtonEnabled() {
return closeButtonEnabled.getValue();
}
private final BooleanProperty detachable = new SimpleBooleanProperty(this, "detachable", true);
/**
* Determines if the popover is detachable at all.
*/
public final BooleanProperty detachableProperty() {
return detachable;
}
/**
* Sets the value of the detachable property.
*
* @param detachable if true then the user can detach / tear off the popover
*
* @see #detachableProperty()
*/
public final void setDetachable(boolean detachable) {
detachableProperty().set(detachable);
}
/**
* Returns the value of the detachable property.
*
* @return true if the user is allowed to detach / tear off the popover
*
* @see #detachableProperty()
*/
public final boolean isDetachable() {
return detachableProperty().get();
}
private final BooleanProperty detached = new SimpleBooleanProperty(this, "detached", false);
/**
* Determines whether the popover is detached from the owning node or not.
* A detached popover no longer shows an arrow pointing at the owner and
* features its own title bar.
*
* @return the detached property
*/
public final BooleanProperty detachedProperty() {
return detached;
}
/**
* Sets the value of the detached property.
*
* @param detached if true the popover will change its appearance to "detached"
* mode
*
* @see #detachedProperty()
*/
public final void setDetached(boolean detached) {
detachedProperty().set(detached);
}
/**
* Returns the value of the detached property.
*
* @return true if the popover is currently detached.
*
* @see #detachedProperty()
*/
public final boolean isDetached() {
return detachedProperty().get();
}
private final DoubleProperty arrowSize = new SimpleDoubleProperty(this, "arrowSize", 12);
/**
* Controls the size of the arrow. Default value is 12.
*
* @return the arrow size property
*/
public final DoubleProperty arrowSizeProperty() {
return arrowSize;
}
/**
* Returns the value of the arrow size property.
*
* @return the arrow size property value
*
* @see #arrowSizeProperty()
*/
public final double getArrowSize() {
return arrowSizeProperty().get();
}
/**
* Sets the value of the arrow size property.
*
* @param size the new value of the arrow size property
*
* @see #arrowSizeProperty()
*/
public final void setArrowSize(double size) {
arrowSizeProperty().set(size);
}
private final DoubleProperty arrowIndent = new SimpleDoubleProperty(this, "arrowIndent", 12);
/**
* Controls the distance between the arrow and the corners of the popover.
* The default value is 12.
*
* @return the arrow indent property
*/
public final DoubleProperty arrowIndentProperty() {
return arrowIndent;
}
/**
* Returns the value of the arrow indent property.
*
* @return the arrow indent value
*
* @see #arrowIndentProperty()
*/
public final double getArrowIndent() {
return arrowIndentProperty().get();
}
/**
* Sets the value of the arrow indent property.
*
* @param size the arrow indent value
*
* @see #arrowIndentProperty()
*/
public final void setArrowIndent(double size) {
arrowIndentProperty().set(size);
}
private final DoubleProperty cornerRadius = new SimpleDoubleProperty(this, "cornerRadius", 6);
/**
* Returns the corner radius property for the popover.
*
* @return the corner radius property (default is 6)
*/
public final DoubleProperty cornerRadiusProperty() {
return cornerRadius;
}
/**
* Returns the value of the corner radius property.
*
* @return the corner radius
*
* @see #cornerRadiusProperty()
*/
public final double getCornerRadius() {
return cornerRadiusProperty().get();
}
/**
* Sets the value of the corner radius property.
*
* @param radius the corner radius
*
* @see #cornerRadiusProperty()
*/
public final void setCornerRadius(double radius) {
cornerRadiusProperty().set(radius);
}
private final StringProperty title = new SimpleStringProperty(this, "title", "Info");
/**
* Stores the title to display in the Popover's header.
*
* @return the title property
*/
public final StringProperty titleProperty() {
return title;
}
/**
* Returns the value of the title property.
*
* @return the detached title
*
* @see #titleProperty()
*/
public final String getTitle() {
return titleProperty().get();
}
/**
* Sets the value of the title property.
*
* @param title the title to use when detached
*
* @see #titleProperty()
*/
public final void setTitle(String title) {
if (title == null) {
throw new IllegalArgumentException("title can not be null");
}
titleProperty().set(title);
}
private final ObjectProperty<ArrowLocation> arrowLocation = new SimpleObjectProperty<>(this, "arrowLocation", ArrowLocation.LEFT_TOP);
/**
* Stores the preferred arrow location. This might not be the actual
* location of the arrow if auto fix is enabled.
*
* @return the arrow location property
*
* @see #setAutoFix(boolean)
*/
public final ObjectProperty<ArrowLocation> arrowLocationProperty() {
return arrowLocation;
}
/**
* Sets the value of the arrow location property.
*
* @param location the requested location
*
* @see #arrowLocationProperty()
*/
public final void setArrowLocation(ArrowLocation location) {
arrowLocationProperty().set(location);
}
/**
* Returns the value of the arrow location property.
*
* @return the preferred arrow location
*
* @see #arrowLocationProperty()
*/
public final ArrowLocation getArrowLocation() {
return arrowLocationProperty().get();
}
/** All possible arrow locations */
public enum ArrowLocation {
LEFT_TOP,
LEFT_CENTER,
LEFT_BOTTOM,
RIGHT_TOP,
RIGHT_CENTER,
RIGHT_BOTTOM,
TOP_LEFT,
TOP_CENTER,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_CENTER,
BOTTOM_RIGHT
}
/**
* Stores the fade-in duration. This should be set before calling Popover.show(..).
*
* @return the fade-in duration property
*/
public final ObjectProperty<Duration> fadeInDurationProperty() {
return fadeInDuration;
}
/**
* Stores the fade-out duration.
*
* @return the fade-out duration property
*/
public final ObjectProperty<Duration> fadeOutDurationProperty() {
return fadeOutDuration;
}
/**
* Returns the value of the fade-in duration property.
*
* @return the fade-in duration
*
* @see #fadeInDurationProperty()
*/
public final Duration getFadeInDuration() {
return fadeInDurationProperty().get();
}
/**
* Sets the value of the fade-in duration property. This should be set before calling
* Popover.show(..).
*
* @param duration the requested fade-in duration
*
* @see #fadeInDurationProperty()
*/
public final void setFadeInDuration(Duration duration) {
fadeInDurationProperty().setValue(duration);
}
/**
* Returns the value of the fade-out duration property.
*
* @return the fade-out duration
*
* @see #fadeOutDurationProperty()
*/
public final Duration getFadeOutDuration() {
return fadeOutDurationProperty().get();
}
/**
* Sets the value of the fade-out duration property.
*
* @param duration the requested fade-out duration
*
* @see #fadeOutDurationProperty()
*/
public final void setFadeOutDuration(Duration duration) {
fadeOutDurationProperty().setValue(duration);
}
/**
* Stores the "animated" flag. If true then the Popover will be shown / hidden with a short
* fade in / out animation.
*
* @return the "animated" property
*/
public final BooleanProperty animatedProperty() {
return animated;
}
/**
* Returns the value of the "animated" property.
*
* @return true if the Popover will be shown and hidden with a short fade animation
*
* @see #animatedProperty()
*/
public final boolean isAnimated() {
return animatedProperty().get();
}
/**
* Sets the value of the "animated" property.
*
* @param animated if true the Popover will be shown and hidden with a short fade animation
*
* @see #animatedProperty()
*/
public final void setAnimated(boolean animated) {
animatedProperty().set(animated);
}
}

@ -0,0 +1,667 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2013 - 2015, ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.*;
import javafx.stage.Window;
import java.util.ArrayList;
import java.util.List;
import static atlantafx.base.controls.Popover.ArrowLocation;
import static java.lang.Double.MAX_VALUE;
import static javafx.geometry.Pos.TOP_RIGHT;
import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY;
import static javafx.scene.paint.Color.YELLOW;
public class PopoverSkin implements Skin<Popover> {
private static final String DETACHED_STYLE_CLASS = "detached";
private double xOffset;
private double yOffset;
private boolean tornOff;
private final Path path;
private final Path clip;
private final BorderPane content;
private final StackPane titlePane;
private final StackPane stackPane;
private Point2D dragStartLocation;
private final Popover popover;
public PopoverSkin(final Popover popover) {
this.popover = popover;
stackPane = popover.getRoot();
stackPane.setPickOnBounds(false);
Bindings.bindContent(stackPane.getStyleClass(), popover.getStyleClass());
// the min width and height equal (2 * corner radius + 2 * arrow indent + 2 * arrow size)
stackPane.minWidthProperty().bind(
Bindings.add(Bindings.multiply(2, popover.arrowSizeProperty()),
Bindings.add(
Bindings.multiply(2, popover.cornerRadiusProperty()),
Bindings.multiply(2, popover.arrowIndentProperty())
)
)
);
stackPane.minHeightProperty().bind(stackPane.minWidthProperty());
Label title = new Label();
title.textProperty().bind(popover.titleProperty());
title.setMaxSize(MAX_VALUE, MAX_VALUE);
title.setAlignment(Pos.CENTER);
title.getStyleClass().add("text");
Label closeIcon = new Label();
closeIcon.setGraphic(createCloseIcon());
closeIcon.setMaxSize(MAX_VALUE, MAX_VALUE);
closeIcon.setContentDisplay(GRAPHIC_ONLY);
closeIcon.visibleProperty().bind(
popover.closeButtonEnabledProperty().and(
popover.detachedProperty().or(popover.headerAlwaysVisibleProperty())));
closeIcon.getStyleClass().add("icon");
closeIcon.setAlignment(TOP_RIGHT);
closeIcon.getGraphic().setOnMouseClicked(evt -> popover.hide());
titlePane = new StackPane();
titlePane.getChildren().add(title);
titlePane.getChildren().add(closeIcon);
titlePane.getStyleClass().add("title");
content = new BorderPane();
content.setCenter(popover.getContentNode());
content.getStyleClass().add("content");
if (popover.isDetached() || popover.isHeaderAlwaysVisible()) {
content.setTop(titlePane);
}
if (popover.isDetached()) {
popover.getStyleClass().add(DETACHED_STYLE_CLASS);
content.getStyleClass().add(DETACHED_STYLE_CLASS);
}
popover.headerAlwaysVisibleProperty().addListener((o, oV, isVisible) -> {
if (isVisible) {
content.setTop(titlePane);
} else if (!popover.isDetached()) {
content.setTop(null);
}
});
InvalidationListener updatePathListener = observable -> updatePath();
getPopupWindow().xProperty().addListener(updatePathListener);
getPopupWindow().yProperty().addListener(updatePathListener);
popover.arrowLocationProperty().addListener(updatePathListener);
popover.contentNodeProperty().addListener((obs, oldContent, newContent) -> content.setCenter(newContent));
popover.detachedProperty().addListener((value, oldDetached, newDetached) -> {
if (newDetached) {
popover.getStyleClass().add(DETACHED_STYLE_CLASS);
content.getStyleClass().add(DETACHED_STYLE_CLASS);
content.setTop(titlePane);
switch (getSkinnable().getArrowLocation()) {
case LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM -> popover.setAnchorX(
popover.getAnchorX() + popover.getArrowSize()
);
case TOP_LEFT, TOP_CENTER, TOP_RIGHT -> popover.setAnchorY(
popover.getAnchorY() + popover.getArrowSize()
);
}
} else {
popover.getStyleClass().remove(DETACHED_STYLE_CLASS);
content.getStyleClass().remove(DETACHED_STYLE_CLASS);
if (!popover.isHeaderAlwaysVisible()) {
content.setTop(null);
}
}
popover.sizeToScene();
updatePath();
});
path = new Path();
path.getStyleClass().add("border");
path.setManaged(false);
clip = new Path();
// the clip is a path and the path has to be filled with a color,
// otherwise clipping will not work.
clip.setFill(YELLOW);
createPathElements();
updatePath();
final EventHandler<MouseEvent> mousePressedHandler = evt -> {
if (popover.isDetachable() || popover.isDetached()) {
tornOff = false;
xOffset = evt.getScreenX();
yOffset = evt.getScreenY();
dragStartLocation = new Point2D(xOffset, yOffset);
}
};
final EventHandler<MouseEvent> mouseReleasedHandler = evt -> {
if (tornOff && !getSkinnable().isDetached()) {
tornOff = false;
getSkinnable().detach();
}
};
final EventHandler<MouseEvent> mouseDragHandler = evt -> {
if (popover.isDetachable() || popover.isDetached()) {
double deltaX = evt.getScreenX() - xOffset;
double deltaY = evt.getScreenY() - yOffset;
Window window = getSkinnable().getScene().getWindow();
window.setX(window.getX() + deltaX);
window.setY(window.getY() + deltaY);
xOffset = evt.getScreenX();
yOffset = evt.getScreenY();
if (dragStartLocation.distance(xOffset, yOffset) > 20) {
tornOff = true;
updatePath();
} else if (tornOff) {
tornOff = false;
updatePath();
}
}
};
stackPane.setOnMousePressed(mousePressedHandler);
stackPane.setOnMouseDragged(mouseDragHandler);
stackPane.setOnMouseReleased(mouseReleasedHandler);
stackPane.setVisible(false);
stackPane.getChildren().add(path);
stackPane.getChildren().add(content);
content.setClip(clip);
}
@Override
public Node getNode() {
return stackPane;
}
@Override
public Popover getSkinnable() {
return popover;
}
@Override
public void dispose() { }
private Node createCloseIcon() {
Group group = new Group();
group.getStyleClass().add("graphics");
Circle circle = new Circle();
circle.getStyleClass().add("circle");
circle.setRadius(12);
circle.setCenterX(12);
circle.setCenterY(12);
group.getChildren().add(circle);
Line line1 = new Line();
line1.getStyleClass().add("line");
line1.setStartX(8);
line1.setStartY(8);
line1.setEndX(16);
line1.setEndY(16);
group.getChildren().add(line1);
Line line2 = new Line();
line2.getStyleClass().add("line");
line2.setStartX(16);
line2.setStartY(8);
line2.setEndX(8);
line2.setEndY(16);
group.getChildren().add(line2);
return group;
}
private MoveTo moveTo;
private QuadCurveTo topCurveTo, rightCurveTo, bottomCurveTo, leftCurveTo;
private HLineTo lineBTop, lineETop, lineHTop, lineKTop;
private LineTo lineCTop, lineDTop, lineFTop, lineGTop, lineITop, lineJTop;
private VLineTo lineBRight, lineERight, lineHRight, lineKRight;
private LineTo lineCRight, lineDRight, lineFRight, lineGRight, lineIRight,
lineJRight;
private HLineTo lineBBottom, lineEBottom, lineHBottom, lineKBottom;
private LineTo lineCBottom, lineDBottom, lineFBottom, lineGBottom,
lineIBottom, lineJBottom;
private VLineTo lineBLeft, lineELeft, lineHLeft, lineKLeft;
private LineTo lineCLeft, lineDLeft, lineFLeft, lineGLeft, lineILeft,
lineJLeft;
private void createPathElements() {
DoubleProperty centerYProperty = new SimpleDoubleProperty();
DoubleProperty centerXProperty = new SimpleDoubleProperty();
DoubleProperty leftEdgeProperty = new SimpleDoubleProperty();
DoubleProperty leftEdgePlusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty topEdgeProperty = new SimpleDoubleProperty();
DoubleProperty topEdgePlusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty rightEdgeProperty = new SimpleDoubleProperty();
DoubleProperty rightEdgeMinusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty bottomEdgeProperty = new SimpleDoubleProperty();
DoubleProperty bottomEdgeMinusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty cornerProperty = getSkinnable().cornerRadiusProperty();
DoubleProperty arrowSizeProperty = getSkinnable().arrowSizeProperty();
DoubleProperty arrowIndentProperty = getSkinnable()
.arrowIndentProperty();
centerYProperty.bind(Bindings.divide(stackPane.heightProperty(), 2));
centerXProperty.bind(Bindings.divide(stackPane.widthProperty(), 2));
leftEdgePlusRadiusProperty.bind(Bindings.add(leftEdgeProperty,
getSkinnable().cornerRadiusProperty()));
topEdgePlusRadiusProperty.bind(Bindings.add(topEdgeProperty,
getSkinnable().cornerRadiusProperty()));
rightEdgeProperty.bind(stackPane.widthProperty());
rightEdgeMinusRadiusProperty.bind(Bindings.subtract(rightEdgeProperty,
getSkinnable().cornerRadiusProperty()));
bottomEdgeProperty.bind(stackPane.heightProperty());
bottomEdgeMinusRadiusProperty.bind(Bindings.subtract(
bottomEdgeProperty, getSkinnable().cornerRadiusProperty()));
// == INIT ==
moveTo = new MoveTo();
moveTo.xProperty().bind(leftEdgePlusRadiusProperty);
moveTo.yProperty().bind(topEdgeProperty);
// == TOP EDGE ==
lineBTop = new HLineTo();
lineBTop.xProperty().bind(
Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty));
lineCTop = new LineTo();
lineCTop.xProperty().bind(
Bindings.add(lineBTop.xProperty(), arrowSizeProperty));
lineCTop.yProperty().bind(
Bindings.subtract(topEdgeProperty, arrowSizeProperty));
lineDTop = new LineTo();
lineDTop.xProperty().bind(
Bindings.add(lineCTop.xProperty(), arrowSizeProperty));
lineDTop.yProperty().bind(topEdgeProperty);
lineETop = new HLineTo();
lineETop.xProperty().bind(
Bindings.subtract(centerXProperty, arrowSizeProperty));
lineFTop = new LineTo();
lineFTop.xProperty().bind(centerXProperty);
lineFTop.yProperty().bind(
Bindings.subtract(topEdgeProperty, arrowSizeProperty));
lineGTop = new LineTo();
lineGTop.xProperty().bind(
Bindings.add(centerXProperty, arrowSizeProperty));
lineGTop.yProperty().bind(topEdgeProperty);
lineHTop = new HLineTo();
lineHTop.xProperty().bind(
Bindings.subtract(Bindings.subtract(
rightEdgeMinusRadiusProperty, arrowIndentProperty),
Bindings.multiply(arrowSizeProperty, 2)));
lineITop = new LineTo();
lineITop.xProperty().bind(
Bindings.subtract(Bindings.subtract(
rightEdgeMinusRadiusProperty, arrowIndentProperty),
arrowSizeProperty));
lineITop.yProperty().bind(
Bindings.subtract(topEdgeProperty, arrowSizeProperty));
lineJTop = new LineTo();
lineJTop.xProperty().bind(
Bindings.subtract(rightEdgeMinusRadiusProperty,
arrowIndentProperty));
lineJTop.yProperty().bind(topEdgeProperty);
lineKTop = new HLineTo();
lineKTop.xProperty().bind(rightEdgeMinusRadiusProperty);
// == RIGHT EDGE ==
rightCurveTo = new QuadCurveTo();
rightCurveTo.xProperty().bind(rightEdgeProperty);
rightCurveTo.yProperty().bind(
Bindings.add(topEdgeProperty, cornerProperty));
rightCurveTo.controlXProperty().bind(rightEdgeProperty);
rightCurveTo.controlYProperty().bind(topEdgeProperty);
lineBRight = new VLineTo();
lineBRight.yProperty().bind(
Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty));
lineCRight = new LineTo();
lineCRight.xProperty().bind(
Bindings.add(rightEdgeProperty, arrowSizeProperty));
lineCRight.yProperty().bind(
Bindings.add(lineBRight.yProperty(), arrowSizeProperty));
lineDRight = new LineTo();
lineDRight.xProperty().bind(rightEdgeProperty);
lineDRight.yProperty().bind(
Bindings.add(lineCRight.yProperty(), arrowSizeProperty));
lineERight = new VLineTo();
lineERight.yProperty().bind(
Bindings.subtract(centerYProperty, arrowSizeProperty));
lineFRight = new LineTo();
lineFRight.xProperty().bind(
Bindings.add(rightEdgeProperty, arrowSizeProperty));
lineFRight.yProperty().bind(centerYProperty);
lineGRight = new LineTo();
lineGRight.xProperty().bind(rightEdgeProperty);
lineGRight.yProperty().bind(
Bindings.add(centerYProperty, arrowSizeProperty));
lineHRight = new VLineTo();
lineHRight.yProperty().bind(
Bindings.subtract(Bindings.subtract(
bottomEdgeMinusRadiusProperty, arrowIndentProperty),
Bindings.multiply(arrowSizeProperty, 2)));
lineIRight = new LineTo();
lineIRight.xProperty().bind(
Bindings.add(rightEdgeProperty, arrowSizeProperty));
lineIRight.yProperty().bind(
Bindings.subtract(Bindings.subtract(
bottomEdgeMinusRadiusProperty, arrowIndentProperty),
arrowSizeProperty));
lineJRight = new LineTo();
lineJRight.xProperty().bind(rightEdgeProperty);
lineJRight.yProperty().bind(
Bindings.subtract(bottomEdgeMinusRadiusProperty,
arrowIndentProperty));
lineKRight = new VLineTo();
lineKRight.yProperty().bind(bottomEdgeMinusRadiusProperty);
// == BOTTOM EDGE ==
bottomCurveTo = new QuadCurveTo();
bottomCurveTo.xProperty().bind(rightEdgeMinusRadiusProperty);
bottomCurveTo.yProperty().bind(bottomEdgeProperty);
bottomCurveTo.controlXProperty().bind(rightEdgeProperty);
bottomCurveTo.controlYProperty().bind(bottomEdgeProperty);
lineBBottom = new HLineTo();
lineBBottom.xProperty().bind(
Bindings.subtract(rightEdgeMinusRadiusProperty,
arrowIndentProperty));
lineCBottom = new LineTo();
lineCBottom.xProperty().bind(
Bindings.subtract(lineBBottom.xProperty(), arrowSizeProperty));
lineCBottom.yProperty().bind(
Bindings.add(bottomEdgeProperty, arrowSizeProperty));
lineDBottom = new LineTo();
lineDBottom.xProperty().bind(
Bindings.subtract(lineCBottom.xProperty(), arrowSizeProperty));
lineDBottom.yProperty().bind(bottomEdgeProperty);
lineEBottom = new HLineTo();
lineEBottom.xProperty().bind(
Bindings.add(centerXProperty, arrowSizeProperty));
lineFBottom = new LineTo();
lineFBottom.xProperty().bind(centerXProperty);
lineFBottom.yProperty().bind(
Bindings.add(bottomEdgeProperty, arrowSizeProperty));
lineGBottom = new LineTo();
lineGBottom.xProperty().bind(
Bindings.subtract(centerXProperty, arrowSizeProperty));
lineGBottom.yProperty().bind(bottomEdgeProperty);
lineHBottom = new HLineTo();
lineHBottom.xProperty().bind(
Bindings.add(Bindings.add(leftEdgePlusRadiusProperty,
arrowIndentProperty), Bindings.multiply(
arrowSizeProperty, 2)));
lineIBottom = new LineTo();
lineIBottom.xProperty().bind(
Bindings.add(Bindings.add(leftEdgePlusRadiusProperty,
arrowIndentProperty), arrowSizeProperty));
lineIBottom.yProperty().bind(
Bindings.add(bottomEdgeProperty, arrowSizeProperty));
lineJBottom = new LineTo();
lineJBottom.xProperty().bind(
Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty));
lineJBottom.yProperty().bind(bottomEdgeProperty);
lineKBottom = new HLineTo();
lineKBottom.xProperty().bind(leftEdgePlusRadiusProperty);
// == LEFT EDGE ==
leftCurveTo = new QuadCurveTo();
leftCurveTo.xProperty().bind(leftEdgeProperty);
leftCurveTo.yProperty().bind(
Bindings.subtract(bottomEdgeProperty, cornerProperty));
leftCurveTo.controlXProperty().bind(leftEdgeProperty);
leftCurveTo.controlYProperty().bind(bottomEdgeProperty);
lineBLeft = new VLineTo();
lineBLeft.yProperty().bind(
Bindings.subtract(bottomEdgeMinusRadiusProperty,
arrowIndentProperty));
lineCLeft = new LineTo();
lineCLeft.xProperty().bind(
Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
lineCLeft.yProperty().bind(
Bindings.subtract(lineBLeft.yProperty(), arrowSizeProperty));
lineDLeft = new LineTo();
lineDLeft.xProperty().bind(leftEdgeProperty);
lineDLeft.yProperty().bind(
Bindings.subtract(lineCLeft.yProperty(), arrowSizeProperty));
lineELeft = new VLineTo();
lineELeft.yProperty().bind(
Bindings.add(centerYProperty, arrowSizeProperty));
lineFLeft = new LineTo();
lineFLeft.xProperty().bind(
Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
lineFLeft.yProperty().bind(centerYProperty);
lineGLeft = new LineTo();
lineGLeft.xProperty().bind(leftEdgeProperty);
lineGLeft.yProperty().bind(
Bindings.subtract(centerYProperty, arrowSizeProperty));
lineHLeft = new VLineTo();
lineHLeft.yProperty().bind(
Bindings.add(Bindings.add(topEdgePlusRadiusProperty,
arrowIndentProperty), Bindings.multiply(
arrowSizeProperty, 2)));
lineILeft = new LineTo();
lineILeft.xProperty().bind(
Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
lineILeft.yProperty().bind(
Bindings.add(Bindings.add(topEdgePlusRadiusProperty,
arrowIndentProperty), arrowSizeProperty));
lineJLeft = new LineTo();
lineJLeft.xProperty().bind(leftEdgeProperty);
lineJLeft.yProperty().bind(
Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty));
lineKLeft = new VLineTo();
lineKLeft.yProperty().bind(topEdgePlusRadiusProperty);
topCurveTo = new QuadCurveTo();
topCurveTo.xProperty().bind(leftEdgePlusRadiusProperty);
topCurveTo.yProperty().bind(topEdgeProperty);
topCurveTo.controlXProperty().bind(leftEdgeProperty);
topCurveTo.controlYProperty().bind(topEdgeProperty);
}
private Window getPopupWindow() {
return getSkinnable().getScene().getWindow();
}
private boolean showArrow(ArrowLocation location) {
ArrowLocation arrowLocation = getSkinnable().getArrowLocation();
return location.equals(arrowLocation) && !getSkinnable().isDetached() && !tornOff;
}
private void updatePath() {
List<PathElement> elements = new ArrayList<>();
elements.add(moveTo);
if (showArrow(ArrowLocation.TOP_LEFT)) {
elements.add(lineBTop);
elements.add(lineCTop);
elements.add(lineDTop);
}
if (showArrow(ArrowLocation.TOP_CENTER)) {
elements.add(lineETop);
elements.add(lineFTop);
elements.add(lineGTop);
}
if (showArrow(ArrowLocation.TOP_RIGHT)) {
elements.add(lineHTop);
elements.add(lineITop);
elements.add(lineJTop);
}
elements.add(lineKTop);
elements.add(rightCurveTo);
if (showArrow(ArrowLocation.RIGHT_TOP)) {
elements.add(lineBRight);
elements.add(lineCRight);
elements.add(lineDRight);
}
if (showArrow(ArrowLocation.RIGHT_CENTER)) {
elements.add(lineERight);
elements.add(lineFRight);
elements.add(lineGRight);
}
if (showArrow(ArrowLocation.RIGHT_BOTTOM)) {
elements.add(lineHRight);
elements.add(lineIRight);
elements.add(lineJRight);
}
elements.add(lineKRight);
elements.add(bottomCurveTo);
if (showArrow(ArrowLocation.BOTTOM_RIGHT)) {
elements.add(lineBBottom);
elements.add(lineCBottom);
elements.add(lineDBottom);
}
if (showArrow(ArrowLocation.BOTTOM_CENTER)) {
elements.add(lineEBottom);
elements.add(lineFBottom);
elements.add(lineGBottom);
}
if (showArrow(ArrowLocation.BOTTOM_LEFT)) {
elements.add(lineHBottom);
elements.add(lineIBottom);
elements.add(lineJBottom);
}
elements.add(lineKBottom);
elements.add(leftCurveTo);
if (showArrow(ArrowLocation.LEFT_BOTTOM)) {
elements.add(lineBLeft);
elements.add(lineCLeft);
elements.add(lineDLeft);
}
if (showArrow(ArrowLocation.LEFT_CENTER)) {
elements.add(lineELeft);
elements.add(lineFLeft);
elements.add(lineGLeft);
}
if (showArrow(ArrowLocation.LEFT_TOP)) {
elements.add(lineHLeft);
elements.add(lineILeft);
elements.add(lineJLeft);
}
elements.add(lineKLeft);
elements.add(topCurveTo);
path.getElements().setAll(elements);
clip.getElements().setAll(elements);
}
}

@ -0,0 +1,45 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.geometry.Orientation;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
public class Spacer extends Region {
public Spacer() {
this(Orientation.HORIZONTAL);
}
public Spacer(Orientation orientation) {
super();
switch (orientation) {
case HORIZONTAL -> HBox.setHgrow(this, Priority.ALWAYS);
case VERTICAL -> VBox.setVgrow(this, Priority.ALWAYS);
}
}
public Spacer(double size) {
this(size, Orientation.HORIZONTAL);
}
public Spacer(double size, Orientation orientation) {
super();
switch (orientation) {
case HORIZONTAL -> {
setMinWidth(size);
setPrefWidth(size);
setMaxWidth(size);
}
case VERTICAL -> {
setMinHeight(size);
setPrefHeight(size);
setMaxHeight(size);
}
}
}
}

@ -0,0 +1,125 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2015, ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.css.PseudoClass;
import javafx.event.ActionEvent;
import javafx.scene.control.Labeled;
import javafx.scene.control.Skin;
@SuppressWarnings("unused")
public class ToggleSwitch extends Labeled {
protected static final String DEFAULT_STYLE_CLASS = "toggle-switch";
protected static final PseudoClass PSEUDO_CLASS_SELECTED = PseudoClass.getPseudoClass("selected");
/** Creates a toggle switch with empty string for its label. */
public ToggleSwitch() {
initialize();
}
/**
* Creates a toggle switch with the specified label
*
* @param text The label string of the control
*/
public ToggleSwitch(String text) {
super(text);
initialize();
}
private void initialize() {
getStyleClass().setAll(DEFAULT_STYLE_CLASS);
}
///////////////////////////////////////////////////////////////////////////
// Properties //
///////////////////////////////////////////////////////////////////////////
/** Indicates whether this switch is selected. */
private BooleanProperty selected;
/** Sets the selected value. */
public final void setSelected(boolean value) {
selectedProperty().set(value);
}
/** Returns whether this Toggle Switch is selected. */
public final boolean isSelected() {
return selected != null && selected.get();
}
/** Returns the selected property. */
public final BooleanProperty selectedProperty() {
if (selected == null) {
selected = new BooleanPropertyBase() {
@Override
protected void invalidated() {
final boolean v = get();
pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, v);
}
@Override
public Object getBean() {
return ToggleSwitch.this;
}
@Override
public String getName() {
return "selected";
}
};
}
return selected;
}
///////////////////////////////////////////////////////////////////////////
// Methods //
///////////////////////////////////////////////////////////////////////////
/**
* Toggles the state of the {@code Switch}. The {@code Switch} will cycle
* through the selected and unselected states.
*/
public void fire() {
if (!isDisabled()) {
setSelected(!isSelected());
fireEvent(new ActionEvent());
}
}
/** {@inheritDoc} */
@Override
protected Skin<?> createDefaultSkin() {
return new ToggleSwitchSkin(this);
}
}

@ -0,0 +1,249 @@
/*
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2015, 2020, 2021, ControlsFX
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package atlantafx.base.controls;
import javafx.animation.Animation;
import javafx.animation.TranslateTransition;
import javafx.beans.property.DoubleProperty;
import javafx.beans.value.WritableValue;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.SizeConverter;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> {
protected final StackPane thumb;
protected final StackPane thumbArea;
protected final Label label;
protected final StackPane labelContainer;
protected final TranslateTransition transition;
public ToggleSwitchSkin(ToggleSwitch control) {
super(control);
thumb = new StackPane();
thumbArea = new StackPane();
label = new Label();
labelContainer = new StackPane();
labelContainer.getStyleClass().add("label-container");
transition = new TranslateTransition(Duration.millis(getThumbMoveAnimationTime()), thumb);
transition.setFromX(0.0);
label.textProperty().bind(control.textProperty());
getChildren().addAll(labelContainer, thumbArea, thumb);
labelContainer.getChildren().addAll(label);
StackPane.setAlignment(label, Pos.CENTER_LEFT);
thumb.getStyleClass().setAll("thumb");
thumbArea.getStyleClass().setAll("thumb-area");
thumbArea.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control));
thumb.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control));
control.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.booleanValue() != oldValue.booleanValue())
selectedStateChanged();
});
}
protected void selectedStateChanged() {
// stop the transition if it was already running, has no effect otherwise
transition.stop();
if (getSkinnable().isSelected()) {
transition.setRate(1.0);
transition.jumpTo(Duration.ZERO);
} else {
// if we are not selected, we need to go from right to left
transition.setRate(-1.0);
transition.jumpTo(transition.getDuration());
}
transition.play();
}
private void mousePressedOnToggleSwitch(ToggleSwitch toggleSwitch) {
toggleSwitch.setSelected(!toggleSwitch.isSelected());
}
/**
* How many milliseconds it should take for the thumb to go from
* one edge to the other
*/
private DoubleProperty thumbMoveAnimationTime = null;
private DoubleProperty thumbMoveAnimationTimeProperty() {
if (thumbMoveAnimationTime == null) {
thumbMoveAnimationTime = new StyleableDoubleProperty(200) {
@Override
public Object getBean() {
return ToggleSwitchSkin.this;
}
@Override
public String getName() {
return "thumbMoveAnimationTime";
}
@Override
public CssMetaData<ToggleSwitch, Number> getCssMetaData() {
return THUMB_MOVE_ANIMATION_TIME;
}
};
}
return thumbMoveAnimationTime;
}
protected double getThumbMoveAnimationTime() {
return thumbMoveAnimationTime == null ? 200 : thumbMoveAnimationTime.get();
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
ToggleSwitch toggleSwitch = getSkinnable();
double thumbWidth = snapSizeX(thumb.prefWidth(-1));
double thumbHeight = snapSizeX(thumb.prefHeight(-1));
thumb.resize(thumbWidth, thumbHeight);
double thumbAreaWidth = snapSizeX(thumbArea.prefWidth(-1));
double thumbAreaHeight = snapSizeX(thumbArea.prefHeight(-1));
double thumbAreaY = snapPositionX(contentY + (contentHeight / 2) - (thumbAreaHeight / 2));
thumbArea.resize(thumbAreaWidth, thumbAreaHeight);
thumbArea.setLayoutX(contentWidth - thumbAreaWidth);
thumbArea.setLayoutY(thumbAreaY);
labelContainer.resize(contentWidth - thumbAreaWidth, thumbAreaHeight);
labelContainer.setLayoutY(thumbAreaY);
// layout the thumb on the "unselected" position
thumb.setLayoutX(thumbArea.getLayoutX());
thumb.setLayoutY(thumbAreaY + (thumbAreaHeight - thumbHeight) / 2);
// each time the layout is done, recompute the thumb "selected" position and apply it to the transition target
final double thumbTarget = thumbAreaWidth - thumbWidth;
transition.setToX(thumbTarget);
if (transition.getStatus() == Animation.Status.RUNNING) {
// if the transition is running, it must be restarted for the value to be properly updated
final Duration currentTime = transition.getCurrentTime();
transition.stop();
transition.playFrom(currentTime);
} else {
// if the transition is not running, simply apply the translateX value
thumb.setTranslateX(toggleSwitch.isSelected() ? thumbTarget : 0.0);
}
}
@Override
protected double computeMinWidth(double height, double topInset, double rightInset,
double bottomInset, double leftInset) {
return leftInset + label.prefWidth(-1) + thumbArea.prefWidth(-1) + rightInset;
}
@Override
protected double computeMinHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
return topInset + Math.max(thumb.prefHeight(-1), label.prefHeight(-1)) + bottomInset;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset,
double bottomInset, double leftInset) {
return leftInset + label.prefWidth(-1) + 1 + thumbArea.prefWidth(-1) + rightInset;
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
return computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
@Override
protected double computeMaxWidth(double height, double topInset, double rightInset,
double bottomInset, double leftInset) {
return getSkinnable().prefWidth(height);
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) {
return getSkinnable().prefHeight(width);
}
private static final CssMetaData<ToggleSwitch, Number> THUMB_MOVE_ANIMATION_TIME =
new CssMetaData<>("-fx-thumb-move-animation-time", SizeConverter.getInstance(), 200) {
@Override
public boolean isSettable(ToggleSwitch toggleSwitch) {
final ToggleSwitchSkin skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
return skin.thumbMoveAnimationTime == null || skin.thumbMoveAnimationTime.isBound();
}
@Override
@SuppressWarnings("RedundantCast")
public StyleableProperty<Number> getStyleableProperty(ToggleSwitch toggleSwitch) {
final ToggleSwitchSkin skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
return (StyleableProperty<Number>) (WritableValue<Number>) skin.thumbMoveAnimationTimeProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(SkinBase.getClassCssMetaData());
styleables.add(THUMB_MOVE_ANIMATION_TIME);
STYLEABLES = Collections.unmodifiableList(styleables);
}
/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
*/
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return STYLEABLES;
}
/**
* {@inheritDoc}
*/
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
}

@ -0,0 +1,40 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.theme;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
public abstract class AbstractTheme implements Theme {
private final Set<URI> stylesheets;
public AbstractTheme() {
this(new LinkedHashSet<>());
}
public AbstractTheme(URI... stylesheets) {
this(Set.of(stylesheets));
}
public AbstractTheme(Set<URI> stylesheets) {
this.stylesheets = Objects.requireNonNull(stylesheets);
}
@Override
public Set<URI> getStylesheets() {
return stylesheets;
}
@Override
public String toString() {
return getClass().getSimpleName() +
"{" +
"name=" + getName() +
", userAgentStylesheet=" + getUserAgentStylesheet() +
", stylesheets=" + stylesheets +
", isDarkMode=" + isDarkMode() +
'}';
}
}

@ -0,0 +1,33 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.theme;
import java.net.URI;
import java.util.Set;
public class PrimerDark extends AbstractTheme {
public PrimerDark() {}
public PrimerDark(URI... stylesheets) {
super(stylesheets);
}
public PrimerDark(Set<URI> stylesheets) {
super(stylesheets);
}
@Override
public String getName() {
return "Primer Dark";
}
@Override
public String getUserAgentStylesheet() {
return "/atlantafx/base/theme/primer-dark.css";
}
@Override
public boolean isDarkMode() {
return true;
}
}

@ -0,0 +1,33 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.theme;
import java.net.URI;
import java.util.Set;
public class PrimerLight extends AbstractTheme {
public PrimerLight() {}
public PrimerLight(URI... stylesheets) {
super(stylesheets);
}
public PrimerLight(Set<URI> stylesheets) {
super(stylesheets);
}
@Override
public String getName() {
return "Primer Light";
}
@Override
public String getUserAgentStylesheet() {
return "/atlantafx/base/theme/primer-light.css";
}
@Override
public boolean isDarkMode() {
return false;
}
}

@ -0,0 +1,113 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.theme;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import java.util.Objects;
@SuppressWarnings("unused")
public final class Styles {
// @formatter:off
// Colors
public static final String ACCENT = "accent";
public static final String SUCCESS = "success";
public static final String WARNING = "warning";
public static final String DANGER = "danger";
public static final PseudoClass STATE_ACCENT = PseudoClass.getPseudoClass(ACCENT);
public static final PseudoClass STATE_SUCCESS = PseudoClass.getPseudoClass(SUCCESS);
public static final PseudoClass STATE_WARNING = PseudoClass.getPseudoClass(WARNING);
public static final PseudoClass STATE_DANGER = PseudoClass.getPseudoClass(DANGER);
// Controls
public static final String TEXT = "text";
public static final String FONT_ICON = "font-icon";
public static final String BUTTON_CIRCLE = "button-circle";
public static final String BUTTON_ICON = "button-icon";
public static final String BUTTON_OUTLINED = "button-outlined";
public static final String LEFT_PILL = "left-pill";
public static final String CENTER_PILL = "center-pill";
public static final String RIGHT_PILL = "right-pill";
public static final String SMALL = "small";
public static final String MEDIUM = "medium";
public static final String LARGE = "large";
public static final String TOP = "top";
public static final String RIGHT = "right";
public static final String BOTTOM = "bottom";
public static final String LEFT = "left";
public static final String CENTER = "center";
public static final String FLAT = "flat";
public static final String BORDERED = "bordered";
public static final String DENSE = "dense";
public static final String ELEVATED_1 = "elevated-1";
public static final String ELEVATED_2 = "elevated-2";
public static final String ELEVATED_3 = "elevated-3";
public static final String ELEVATED_4 = "elevated-4";
public static final String INTERACTIVE = "interactive";
public static final String STRIPED = "striped";
// Text
public static final String TITLE_1 = "title-1";
public static final String TITLE_2 = "title-2";
public static final String TITLE_3 = "title-3";
public static final String TITLE_4 = "title-4";
public static final String TEXT_CAPTION = "text-caption";
public static final String TEXT_SMALL = "text-small";
public static final String TEXT_BOLD = "text-bold";
public static final String TEXT_BOLDER = "text-bolder";
public static final String TEXT_NORMAL = "text-normal";
public static final String TEXT_LIGHTER = "text-lighter";
public static final String TEXT_ITALIC = "text-italic";
public static final String TEXT_OBLIQUE = "text-oblique";
public static final String TEXT_STRIKETHROUGH = "text-strikethrough";
public static final String TEXT_UNDERLINED = "text-underlined";
// @formatter:on
public static void toggleStyleClass(Node node, String styleClass) {
Objects.requireNonNull(node);
Objects.requireNonNull(styleClass);
int idx = node.getStyleClass().indexOf(styleClass);
if (idx > 0) {
node.getStyleClass().remove(idx);
} else {
node.getStyleClass().add(styleClass);
}
}
public static void addStyleClass(Node node, String styleClass, String... excludes) {
Objects.requireNonNull(node);
Objects.requireNonNull(styleClass);
if (excludes != null && excludes.length > 0) {
node.getStyleClass().removeAll(excludes);
}
node.getStyleClass().add(styleClass);
}
public static void activatePseudoClass(Node node, PseudoClass pseudoClass, PseudoClass... excludes) {
Objects.requireNonNull(node);
Objects.requireNonNull(pseudoClass);
if (excludes != null && excludes.length > 0) {
for (PseudoClass exclude : excludes) {
node.pseudoClassStateChanged(exclude, false);
}
}
node.pseudoClassStateChanged(pseudoClass, true);
}
}

@ -0,0 +1,26 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.theme;
import java.net.URI;
import java.util.Set;
import static javafx.application.Application.STYLESHEET_CASPIAN;
import static javafx.application.Application.STYLESHEET_MODENA;
// This is merely a wrapper around stylesheet paths.
// Let's hope JavaFX theme support will be merged.
// https://github.com/openjdk/jfx/pull/511
public interface Theme {
String getName();
String getUserAgentStylesheet();
Set<URI> getStylesheets();
boolean isDarkMode();
default boolean isDefault() {
return STYLESHEET_MODENA.equals(getUserAgentStylesheet()) || STYLESHEET_CASPIAN.equals(getUserAgentStylesheet());
}
}

@ -0,0 +1,181 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.util;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
/**
* Converts between user-edited strings and {@link Double} values.
* Accepts an optional {@link Runnable} that resets the editor on {@link NumberFormatException},
* or a {@link TextField} or {@link Spinner} that is preemptively monitored for invalid
* input during typing, and restricts valid input to a specified range when committed.
* <p>
* This implementation shows up to two decimal digits, but only if a fractional part exists.
* The default implementation always shows one decimal digit which hinders typing.</p>
*
* @author Christoph Nahr
* @version 1.0.2
*/
public class DoubleStringConverter extends StringConverter<Double> {
private final DecimalFormat _format = new DecimalFormat("0.##");
private Runnable _reset;
/**
* Creates a {@link DoubleStringConverter}.
* Swallows {@link NumberFormatException} but does nothing
* in response until {@link #setReset} is defined.
*/
public DoubleStringConverter() { }
/**
* Creates a {@link DoubleStringConverter} with an editor reset callback.
* Specifying {@code null} has the same effect as the default constructor.
*
* @param reset the {@link Runnable} to call upon {@link NumberFormatException}
*/
public DoubleStringConverter(Runnable reset) {
_reset = reset;
}
/**
* Creates a {@link DoubleStringConverter} with the specified input range.
* Preemptively monitors {@code input} to reject any invalid characters during
* typing, restricts {@code input} to [{@code min}, {@code max}] (inclusive) when
* valid text is committed, and resets {@code input} to the closest value to zero
* within [{@code min}, {@code max}] when invalid text is committed.
*
* @param input the {@link TextField} providing user-edited strings
* @param min the smallest valid {@link Double} value
* @param max the greatest valid {@link Double} value
* @throws NullPointerException if {@code input} is {@code null}
*/
public DoubleStringConverter(TextField input, double min, double max) {
if (input == null) { throw new NullPointerException("input"); }
final double resetValue = Math.min(Math.max(0, min), max);
_reset = () -> input.setText(_format.format(resetValue));
// bound JavaFX properties cannot be explicitly set
// if (!input.tooltipProperty().isBound()) {
// input.setTooltip(new Tooltip(String.format("Enter a value between %.2f and %.2f", min, max)));
// }
// restrict direct input to valid numerical characters
input.textProperty().addListener((ov, oldValue, newValue) -> {
if (newValue == null || newValue.isEmpty()) { return; }
// special case: minus sign if negative values allowed
if (min < 0 && newValue.endsWith("-")) {
if (newValue.length() > 1) { Platform.runLater(() -> input.setText("-")); }
return;
}
// revert to oldValue if newValue cannot be parsed
try {
Double.parseDouble(newValue);
} catch (NumberFormatException e) {
Platform.runLater(() -> input.setText(oldValue));
}
});
// validate committed input and restrict to legal range
final EventHandler<ActionEvent> oldHandler = input.getOnAction();
input.setOnAction(t -> {
// fromString performs input validation
final double value = fromString(input.getText());
// redundant for Spinner but not harmful
final double restricted = Math.min(Math.max(value, min), max);
if (value != restricted) { input.setText(_format.format(restricted)); }
// required for Spinner which handles onAction
if (oldHandler != null) { oldHandler.handle(t); }
});
}
/**
* Creates a {@link DoubleStringConverter} for the specified {@link Spinner}.
* Uses the {@link TextField} and minimum and maximum values of the specified
* {@link Spinner} for construction, and also sets the new {@link DoubleStringConverter}
* on its {@link SpinnerValueFactory.DoubleSpinnerValueFactory}.
*
* @param spinner the {@link Spinner} to create a {@link DoubleStringConverter} for
* @return the new {@link DoubleStringConverter}
* @throws NullPointerException if {@code spinner} is {@code null}
*/
public static DoubleStringConverter createFor(Spinner<Double> spinner) {
final SpinnerValueFactory.DoubleSpinnerValueFactory factory =
(SpinnerValueFactory.DoubleSpinnerValueFactory) spinner.getValueFactory();
final DoubleStringConverter converter = new DoubleStringConverter(
spinner.getEditor(), factory.getMin(), factory.getMax());
factory.setConverter(converter);
spinner.setTooltip(new Tooltip(String.format(
"Enter a value between %.2f and %.2f",
factory.getMin(), factory.getMax())));
return converter;
}
/**
* Sets the editor reset callback.
* Specify {@code null} to clear a previously set {@link Runnable}. When creating
* a {@link DoubleStringConverter} for a {@link TextField} or {@link Spinner},
* this callback is automatically defined to reset committed invalid input to the
* closest value to zero within the legal range. Setting a different callback
* will overwrite this functionality.
*
* @param reset the {@link Runnable} to call upon {@link NumberFormatException}
* @see #fromString
*/
public void setReset(Runnable reset) {
_reset = reset;
}
/**
* Converts the specified {@link String} into its {@link Double} value.
* A {@code null}, empty, or otherwise invalid argument returns zero
* and also executes the editor reset callback, if any.
*
* @param s the {@link String} to convert
* @return the {@link Double} value of {@code s}
* @see #setReset
*/
@Override
public Double fromString(String s) {
if (s == null || s.isEmpty()) {
if (_reset != null) { _reset.run(); }
return 0.0;
}
try {
return Double.valueOf(s);
} catch (NumberFormatException e) {
if (_reset != null) { _reset.run(); }
return 0.0;
}
}
/**
* Converts the specified {@link Double} into its {@link String} form.
* A {@code null} argument is converted into the literal string "0".
*
* @param value the {@link Double} to convert
* @return the {@link String} form of {@code value}
*/
@Override
public String toString(Double value) {
if (value == null) { return "0"; }
return _format.format(value);
}
}

@ -0,0 +1,175 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.util;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.util.StringConverter;
/**
* Converts between user-edited strings and {@link Integer} values.
* Accepts an optional {@link Runnable} that resets the editor on {@link NumberFormatException},
* or a {@link TextField} or {@link Spinner} that is preemptively monitored for invalid
* input during typing, and restricts valid input to a specified range when committed.
*
* @author Christoph Nahr
* @version 1.0.2
*/
public class IntegerStringConverter extends StringConverter<Integer> {
private Runnable _reset;
/**
* Creates an {@link IntegerStringConverter}.
* Swallows {@link NumberFormatException} but does nothing
* in response until {@link #setReset} is defined.
*/
public IntegerStringConverter() { }
/**
* Creates an {@link IntegerStringConverter} with an editor reset callback.
* Specifying {@code null} has the same effect as the default constructor.
*
* @param reset the {@link Runnable} to call upon {@link NumberFormatException}
*/
public IntegerStringConverter(Runnable reset) {
_reset = reset;
}
/**
* Creates an {@link IntegerStringConverter} with the specified input range.
* Preemptively monitors {@code input} to reject any invalid characters during
* typing, restricts {@code input} to [{@code min}, {@code max}] (inclusive) when
* valid text is committed, and resets {@code input} to the closest value to zero
* within [{@code min}, {@code max}] when invalid text is committed.
*
* @param input the {@link TextField} providing user-edited strings
* @param min the smallest valid {@link Integer} value
* @param max the greatest valid {@link Integer} value
* @throws NullPointerException if {@code input} is {@code null}
*/
public IntegerStringConverter(TextField input, int min, int max) {
if (input == null) { throw new NullPointerException("input"); }
final int resetValue = Math.min(Math.max(0, min), max);
_reset = () -> input.setText(Integer.toString(resetValue));
// bound JavaFX properties cannot be explicitly set
// if (!input.tooltipProperty().isBound()) {
// input.setTooltip(new Tooltip(String.format("Enter a value between %d and %d", min, max)));
// }
// restrict direct input to valid numerical characters
input.textProperty().addListener((ov, oldValue, newValue) -> {
if (newValue == null || newValue.isEmpty()) { return; }
// special case: minus sign if negative values allowed
if (min < 0 && newValue.endsWith("-")) {
if (newValue.length() > 1) { Platform.runLater(() -> input.setText("-")); }
return;
}
// revert to oldValue if newValue cannot be parsed
try {
Integer.parseInt(newValue);
} catch (NumberFormatException e) {
Platform.runLater(() -> input.setText(oldValue));
}
});
// validate committed input and restrict to legal range
final EventHandler<ActionEvent> oldHandler = input.getOnAction();
input.setOnAction(t -> {
// fromString performs input validation
final int value = fromString(input.getText());
// redundant for Spinner but not harmful
final int restricted = Math.min(Math.max(value, min), max);
if (value != restricted) { input.setText(Integer.toString(restricted)); }
// required for Spinner which handles onAction
if (oldHandler != null) { oldHandler.handle(t); }
});
}
/**
* Creates an {@link IntegerStringConverter} for the specified {@link Spinner}.
* Uses the {@link TextField} and minimum and maximum values of the specified
* {@link Spinner} for construction, and also sets the new {@link IntegerStringConverter}
* on its {@link SpinnerValueFactory.IntegerSpinnerValueFactory}.
*
* @param spinner the {@link Spinner} to create an {@link IntegerStringConverter} for
* @return the new {@link IntegerStringConverter}
* @throws NullPointerException if {@code spinner} is {@code null}
*/
public static IntegerStringConverter createFor(Spinner<Integer> spinner) {
final SpinnerValueFactory.IntegerSpinnerValueFactory factory =
(SpinnerValueFactory.IntegerSpinnerValueFactory) spinner.getValueFactory();
final IntegerStringConverter converter = new IntegerStringConverter(
spinner.getEditor(), factory.getMin(), factory.getMax());
factory.setConverter(converter);
spinner.setTooltip(new Tooltip(String.format(
"Enter a value between %d and %d",
factory.getMin(), factory.getMax())));
return converter;
}
/**
* Sets the editor reset callback.
* Specify {@code null} to clear a previously set {@link Runnable}. When creating
* an {@link IntegerStringConverter} for a {@link TextField} or {@link Spinner},
* this callback is automatically defined to reset committed invalid input to the
* closest value to zero within the legal range. Setting a different callback
* will overwrite this functionality.
*
* @param reset the {@link Runnable} to call upon {@link NumberFormatException}
* @see #fromString
*/
public void setReset(Runnable reset) {
_reset = reset;
}
/**
* Converts the specified {@link String} into its {@link Integer} value.
* A {@code null}, empty, or otherwise invalid argument returns zero
* and also executes the editor reset callback, if any.
*
* @param s the {@link String} to convert
* @return the {@link Integer} value of {@code s}
* @see #setReset
*/
@Override
public Integer fromString(String s) {
if (s == null || s.isEmpty()) {
if (_reset != null) { _reset.run(); }
return 0;
}
try {
return Integer.valueOf(s);
} catch (NumberFormatException e) {
if (_reset != null) { _reset.run(); }
return 0;
}
}
/**
* Converts the specified {@link Integer} into its {@link String} form.
* A {@code null} argument is converted into the literal string "0".
*
* @param value the {@link Integer} to convert
* @return the {@link String} form of {@code value}
*/
@Override
public String toString(Integer value) {
if (value == null) { return "0"; }
return Integer.toString(value);
}
}

@ -0,0 +1,12 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.util;
public final class PlatformUtils {
private static final String OS = System.getProperty("os.name");
private static final boolean MAC = OS.startsWith("Mac");
public static boolean isMac() {
return MAC;
}
}

@ -0,0 +1,12 @@
/* SPDX-License-Identifier: MIT */
module atlantafx.base {
requires transitive javafx.controls;
exports atlantafx.base.controls;
exports atlantafx.base.theme;
exports atlantafx.base.util;
opens atlantafx.base.theme;
}

316
mvnw vendored Executable file

@ -0,0 +1,316 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`\\unset -f command; \\command -v java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

188
mvnw.cmd vendored Normal file

@ -0,0 +1,188 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

2514
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

12
package.json Executable file

@ -0,0 +1,12 @@
{
"name": "atlanta",
"version": "1.0.0",
"devDependencies": {
"grunt": "^1.4.1",
"grunt-cli": "^1.4.1",
"grunt-contrib-concat": "^2.0.0",
"grunt-contrib-cssmin": "^4.0.0",
"grunt-sass": "^3.1.0",
"sass": "^1.49.9"
}
}

359
pom.xml Executable file

@ -0,0 +1,359 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-parent</artifactId>
<packaging>pom</packaging>
<version>0.1.0</version>
<name>AtlantaFX</name>
<description>JavaFX CSS theme collection plus additional controls</description>
<url>https://github.com/mkpaz/atlantafx</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://raw.githubusercontent.com/mkpaz/atlantafx/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>mkpaz</id>
<name>mkpaz</name>
</developer>
</developers>
<scm>
<developerConnection>scm:git:https://github.com/mkpaz/atlantafx.git</developerConnection>
<connection>${project.scm.developerConnection}</connection>
<url>${project.url}</url>
</scm>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<modules>
<module>base</module>
<module>sampler</module>
<module>styles</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<java.version>17</java.version>
<openjfx.version>17.0.0.1</openjfx.version>
<nodejs.version>v16.14.2</nodejs.version>
<app.name>AtlantaFX</app.name>
<app.version>${project.version}</app.version>
<lib.commons-lang.version>3.12.0</lib.commons-lang.version>
<lib.cssfx.version>11.5.1</lib.cssfx.version>
<lib.ikonli.version>12.2.0</lib.ikonli.version>
<lib.datafaker.version>1.3.0</lib.datafaker.version>
<lib.jetbrains-annotations.version>22.0.0</lib.jetbrains-annotations.version>
<test.assertj.version>3.21.0</test.assertj.version>
<test.junit.version>5.8.1</test.junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>atlantafx-styles</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>atlantafx-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${openjfx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${openjfx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>${openjfx.version}</version>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
<version>${lib.ikonli.version}</version>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-feather-pack</artifactId>
<version>${lib.ikonli.version}</version>
</dependency>
<dependency>
<groupId>fr.brouillard.oss</groupId>
<artifactId>cssfx</artifactId>
<version>${lib.cssfx.version}</version>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>${lib.datafaker.version}</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${test.assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${test.junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${test.junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.9.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.0.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.0.0-M1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.9.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
<plugin>
<groupId>io.github.wiverson</groupId>
<artifactId>jtoolprovider-plugin</artifactId>
<version>1.0.34</version>
</plugin>
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.RC1</version>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.6</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<configuration>
<nodeVersion>${nodejs.version}</nodeVersion>
<workingDirectory>${project.basedir}</workingDirectory>
</configuration>
<executions>
<execution>
<id>install-node-and-npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>install-npm-packages</id>
<phase>generate-resources</phase>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>linux-active</id>
<activation>
<os>
<family>unix</family>
</os>
</activation>
<properties>
<platform>linux</platform>
</properties>
</profile>
<profile>
<id>windows-active</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<properties>
<platform>win</platform>
</properties>
</profile>
<profile>
<id>sonatype</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<!-- package project sources -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-source</id>
<phase>compile</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- package project javadoc -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- sign deployed artifacts -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

BIN
sampler/icons/app-icon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
sampler/icons/app-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

305
sampler/pom.xml Executable file

@ -0,0 +1,305 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-parent</artifactId>
<version>0.1.0</version>
</parent>
<artifactId>atlantafx-sampler</artifactId>
<properties>
<app.launcher>atlantafx.sampler.Launcher</app.launcher>
<app.module>atlantafx.sampler</app.module>
<!-- final artifact name -->
<build.artifactName>${app.name}-${app.version}-${platform}-${os.arch}</build.artifactName>
<!-- application dependencies that will be included as JAR files -->
<build.dependenciesDir>${project.build.directory}${file.separator}dependencies</build.dependenciesDir>
<!-- application dependencies that will be bundled into runtime image -->
<build.platformModulesDir>${project.build.directory}${file.separator}platform-modules</build.platformModulesDir>
<!-- jpackage generated application image root -->
<build.package.appImageDir>${project.build.directory}${file.separator}app-image</build.package.appImageDir>
<!-- AppDir spec compliant dir for creating AppImage -->
<build.package.appDir>${project.build.directory}${file.separator}app-dir</build.package.appDir>
<!-- jlink generated runtime image -->
<build.package.runtimeImageDir>${project.build.directory}${file.separator}runtime-image</build.package.runtimeImageDir>
<!-- contains package scripts after filtering and placeholder replacement -->
<build.package.scriptsDir>${project.build.directory}${file.separator}package-scripts</build.package.scriptsDir>
<!-- jpackage directory for temp artifacts -->
<build.package.tempDir>${project.build.directory}${file.separator}package-temp</build.package.tempDir>
<!-- final artifacts (zip, deb, rpm...) -->
<build.releaseDir>${project.build.directory}${file.separator}release</build.releaseDir>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>atlantafx-base</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-feather-pack</artifactId>
</dependency>
<dependency>
<groupId>fr.brouillard.oss</groupId>
<artifactId>cssfx</artifactId>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<!-- place all resources under module package prefix -->
<resource>
<directory>src/main/resources</directory>
<targetPath>atlantafx/sampler</targetPath>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<targetPath>atlantafx/sampler</targetPath>
<filtering>true</filtering>
<includes>
<include>application.properties</include>
</includes>
</resource>
<!-- copy pages for source code viewer -->
<resource>
<directory>src/main/java/atlantafx/sampler/page</directory>
<targetPath>atlantafx/sampler/page</targetPath>
<filtering>false</filtering>
<excludes>
<exclude>**/AbstractPage.java</exclude>
<exclude>**/CodeViewer.java</exclude>
<exclude>**/Page.java</exclude>
<exclude>**/SampleBlock.java</exclude>
</excludes>
</resource>
<!-- copy icons -->
<resource>
<directory>icons</directory>
<targetPath>atlantafx/sampler/assets</targetPath>
<filtering>false</filtering>
</resource>
<resource>
<directory>icons</directory>
<filtering>false</filtering>
<targetPath>${project.build.directory}</targetPath>
</resource>
<!-- copy and filter build scripts -->
<resource>
<directory>src/package-scripts</directory>
<filtering>true</filtering>
<targetPath>${build.package.scriptsDir}</targetPath>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<configuration>
<executable>${java.home}/bin/java</executable>
<mainClass>${app.launcher}</mainClass>
</configuration>
<executions>
<execution>
<id>run</id>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<configuration>
<installDirectory>${project.parent.basedir}</installDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<!-- copy all dependencies that won't be included into runtime image -->
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${build.dependenciesDir}</outputDirectory>
<includeScope>runtime</includeScope>
<excludeGroupIds>org.openjfx</excludeGroupIds>
</configuration>
</execution>
<!-- copy platform dependencies that will be included into runtime image -->
<execution>
<id>copy-openjfx</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${build.platformModulesDir}</outputDirectory>
<includeGroupIds>org.openjfx</includeGroupIds>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<configuration>
<!-- skip installation to the local repository -->
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<!-- copy application JAR file to the dependencies -->
<configuration>
<outputDirectory>${build.dependenciesDir}</outputDirectory>
</configuration>
</plugin>
<plugin>
<groupId>io.github.wiverson</groupId>
<artifactId>jtoolprovider-plugin</artifactId>
<executions>
<!-- create custom JRE image -->
<execution>
<id>create-runtime-image</id>
<phase>package</phase>
<goals>
<goal>java-tool</goal>
</goals>
<configuration>
<toolName>jlink</toolName>
<addModules>java.base,java.logging,jdk.localedata,java.desktop,javafx.controls,javafx.swing,javafx.web</addModules>
<modulePath>${build.platformModulesDir}</modulePath>
<output>${build.package.runtimeImageDir}</output>
<args>
<!-- additional options to shrink resulting image even more -->
<arg>--compress=2</arg>
<arg>--include-locales=en</arg>
<arg>--no-header-files</arg>
<arg>--no-man-pages</arg>
<arg>--strip-debug</arg>
<arg>--verbose</arg>
</args>
</configuration>
</execution>
<!-- create application image (includes JRE plus application JARs and resources) -->
<execution>
<id>create-app-image</id>
<phase>package</phase>
<goals>
<goal>java-tool</goal>
</goals>
<configuration>
<toolName>jpackage</toolName>
<removeDirectories>${build.package.tempDir}</removeDirectories>
<args>
<arg>@${build.package.scriptsDir}${file.separator}args-base.txt</arg>
<arg>@${build.package.scriptsDir}${file.separator}args-app-image.txt</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>
<!-- compress app image and place result to the release dir -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assembly</id>
<phase>install</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<outputDirectory>${build.releaseDir}</outputDirectory>
<finalName>${build.artifactName}</finalName>
<attach>false</attach>
<appendAssemblyId>false</appendAssemblyId>
<tarLongFileMode>posix</tarLongFileMode>
<descriptors>
<descriptor>src/package-scripts/app-image.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
<!-- skip module deployment -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>linux-active</id>
<activation>
<os>
<family>unix</family>
</os>
</activation>
<properties>
<app.icon>${project.build.directory}/app-icon.png</app.icon>
<app.build.compressionAlg>tar.gz</app.build.compressionAlg>
</properties>
</profile>
<profile>
<id>windows-active</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<properties>
<app.icon>${project.build.directory}\app-icon.ico</app.icon>
<app.build.compressionAlg>zip</app.build.compressionAlg>
</properties>
</profile>
</profiles>
</project>

@ -0,0 +1,107 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler;
import atlantafx.sampler.layout.ApplicationWindow;
import atlantafx.sampler.theme.ThemeManager;
import fr.brouillard.oss.cssfx.CSSFX;
import fr.brouillard.oss.cssfx.api.URIToPathConverter;
import fr.brouillard.oss.cssfx.impl.log.CSSFXLogger;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.file.Paths;
import java.util.Properties;
import static java.nio.charset.StandardCharsets.UTF_8;
public class Launcher extends Application {
public static final boolean IS_DEV_MODE = "DEV".equalsIgnoreCase(
Resources.getPropertyOrEnv("atlantafx.mode", "ATLANTAFX_MODE")
);
private static class DefaultExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
Thread.currentThread().setUncaughtExceptionHandler(new DefaultExceptionHandler());
if (IS_DEV_MODE) {
System.out.println("[WARNING] Application is running in development mode.");
}
loadApplicationProperties();
var appIcon = new Image(Resources.getResourceAsStream("assets/app-icon.png"));
var root = new ApplicationWindow();
var scene = new Scene(root, 1200, 768);
var tm = ThemeManager.getInstance();
tm.setTheme(scene, tm.getAvailableThemes().get(0));
if (IS_DEV_MODE) { startCssFX(scene); }
scene.getStylesheets().addAll(
Resources.resolve("assets/fonts/index.css"),
Resources.resolve("assets/styles/index.css")
);
stage.setScene(scene);
stage.setTitle(System.getProperty("app.name"));
stage.getIcons().add(appIcon);
stage.setResizable(true);
stage.setOnCloseRequest(t -> Platform.exit());
Platform.runLater(() -> {
stage.show();
stage.requestFocus();
});
}
private void loadApplicationProperties() {
try {
var properties = new Properties();
properties.load(new InputStreamReader(Resources.getResourceAsStream("application.properties"), UTF_8));
properties.forEach((key, value) -> System.setProperty(
String.valueOf(key),
String.valueOf(value)
));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void startCssFX(Scene scene) {
URIToPathConverter fileUrlConverter = uri -> {
try {
if (uri != null && uri.startsWith("file:")) {
return Paths.get(URI.create(uri));
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
};
CSSFX.addConverter(fileUrlConverter).start();
CSSFXLogger.setLoggerFactory(loggerName -> (level, message, args) ->
System.out.println("[CSSFX] " + String.format(message, args))
);
CSSFX.start(scene);
}
}

@ -0,0 +1,35 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Objects;
public final class Resources {
public static final String MODULE_DIR = "/atlantafx/sampler/";
public static InputStream getResourceAsStream(String resource) {
String path = resolve(resource);
return Objects.requireNonNull(
Launcher.class.getResourceAsStream(resolve(path)),
"Resource not found: " + path
);
}
public static URI getResource(String resource) {
String path = resolve(resource);
URL url = Objects.requireNonNull(Launcher.class.getResource(resolve(path)), "Resource not found: " + path);
return URI.create(url.toExternalForm());
}
public static String resolve(String resource) {
Objects.requireNonNull(resource);
return resource.startsWith("/") ? resource : MODULE_DIR + resource;
}
public static String getPropertyOrEnv(String propertyKey, String envKey) {
return System.getProperty(propertyKey, System.getenv(envKey));
}
}

@ -0,0 +1,145 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.fake;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.*;
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 java.util.stream.IntStream;
import static atlantafx.sampler.util.Controls.menuItem;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
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 viewToggleGroup = new ToggleGroup();
var toggleItem1 = new RadioMenuItem("Single");
toggleItem1.setSelected(true);
toggleItem1.setToggleGroup(viewToggleGroup);
var toggleItem2 = new RadioMenuItem("Two Columns");
toggleItem2.setToggleGroup(viewToggleGroup);
var toggleItem3 = new RadioMenuItem("Three Columns");
toggleItem3.setToggleGroup(viewToggleGroup);
viewMenu.getItems().addAll(
showToolbarItem,
showGridItem,
new SeparatorMenuItem(),
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,68 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.fake.domain;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import net.datafaker.Faker;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
public final class Book {
private UUID id;
private final BooleanProperty state;
private String author;
private String title;
private String isbn;
public Book(UUID id,
BooleanProperty state,
String author,
String title,
String isbn) {
this.id = id;
this.state = state;
this.author = author;
this.title = title;
this.isbn = isbn;
}
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public boolean getState() { return state.get(); }
public void setState(boolean state) { this.state.set(state); }
public BooleanProperty stateProperty() { return state; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
public static Book random(Faker faker) {
return new Book(
UUID.randomUUID(),
new SimpleBooleanProperty(),
faker.book().author(),
faker.book().title(),
faker.code().isbn10()
);
}
public String toString(Function<Book, String> f) {
Objects.requireNonNull(f);
return f.apply(this);
}
}

@ -0,0 +1,92 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.fake.domain;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import net.datafaker.Faker;
import java.util.Objects;
import java.util.function.Function;
public final class Product {
private static final int MAX_STOCK_SIZE = 999;
private int id;
private final BooleanProperty state;
private String brand;
private String name;
private String price;
private Integer count;
public Product(int id,
BooleanProperty state,
String brand,
String name,
String price,
Integer count
) {
this.id = id;
this.state = state;
this.brand = brand;
this.name = name;
this.price = price;
this.count = count;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public boolean getState() { return state.get(); }
public BooleanProperty stateProperty() { return state; }
public void setState(boolean state) { this.state.set(state); }
public String getBrand() { return brand; }
public void setBrand(String brand) { this.brand = brand; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPrice() { return price; }
public void setPrice(String price) { this.price = price; }
public Integer getCount() { return count; }
public void setCount(Integer count) { this.count = count; }
public double getAvailability() {
return count * 1.0 / MAX_STOCK_SIZE;
}
public static Product random(int id, Faker faker) {
return new Product(
id,
new SimpleBooleanProperty(),
faker.commerce().brand(),
faker.commerce().productName(),
faker.commerce().price(),
faker.random().nextInt(0, MAX_STOCK_SIZE)
);
}
public static Product random(int id, String brand, Faker faker) {
var product = random(id, faker);
product.setBrand(brand);
return product;
}
public static Product empty(int id) {
return new Product(id, new SimpleBooleanProperty(), "", "", "", 0);
}
public String toString(Function<Product, String> f) {
Objects.requireNonNull(f);
return f.apply(this);
}
}

@ -0,0 +1,38 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.components.OverviewPage;
import javafx.application.Platform;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import static javafx.scene.layout.Priority.ALWAYS;
public class ApplicationWindow extends BorderPane {
public ApplicationWindow() {
var sidebar = new Sidebar();
sidebar.setMinWidth(200);
final var pageContainer = new StackPane();
HBox.setHgrow(pageContainer, ALWAYS);
sidebar.setOnSelect(pageClass -> {
try {
Page page = pageClass.getDeclaredConstructor().newInstance();
pageContainer.getChildren().setAll(page.getView());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// ~
setLeft(sidebar);
setCenter(pageContainer);
sidebar.select(OverviewPage.class);
Platform.runLater(sidebar::requestFocus);
}
}

@ -0,0 +1,215 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.components.*;
import atlantafx.sampler.page.general.ThemePage;
import atlantafx.sampler.page.general.TypographyPage;
import atlantafx.sampler.util.Containers;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.transformation.FilteredList;
import javafx.css.PseudoClass;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import static javafx.scene.layout.Priority.ALWAYS;
public class Sidebar extends VBox {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final FilteredList<Region> navigationMenu = navigationMenu();
private final ReadOnlyObjectWrapper<NavLink> selectedLink = new ReadOnlyObjectWrapper<>();
private Consumer<Class<? extends Page>> navigationHandler;
public Sidebar() {
super();
setId("sidebar");
createView();
selectedLink.addListener((obs, old, val) -> {
if (navigationHandler != null) {
navigationHandler.accept(val != null ? val.getPageClass() : null);
}
});
}
public void select(Class<? extends Page> pageClass) {
navigationMenu.stream()
.filter(region -> region instanceof NavLink link && pageClass.equals(link.getPageClass()))
.findFirst()
.ifPresent(link -> navigate((NavLink) link));
}
public void setOnSelect(Consumer<Class<? extends Page>> c) {
navigationHandler = c;
}
private void createView() {
var navContainer = new VBox();
navContainer.getStyleClass().add("nav-menu");
Bindings.bindContent(navContainer.getChildren(), navigationMenu);
var navScroll = new ScrollPane(navContainer);
Containers.setScrollConstraints(navScroll,
ScrollPane.ScrollBarPolicy.AS_NEEDED, true,
ScrollPane.ScrollBarPolicy.AS_NEEDED, true
);
VBox.setVgrow(navScroll, ALWAYS);
// == SEARCH FORM ==
var searchField = new TextField();
searchField.setPromptText("Search");
HBox.setHgrow(searchField, ALWAYS);
searchField.textProperty().addListener((obs, old, val) -> {
if (val == null || val.isBlank()) {
navigationMenu.setPredicate(c -> true);
return;
}
navigationMenu.setPredicate(c -> c instanceof NavLink link && link.matches(val));
});
var searchForm = new HBox(searchField);
searchForm.setId("search-form");
searchForm.setAlignment(Pos.CENTER_LEFT);
// ~
getChildren().addAll(searchForm, navScroll);
}
private Label caption(String text) {
var label = new Label(text);
label.getStyleClass().add("caption");
label.setMaxWidth(Double.MAX_VALUE);
return label;
}
private FilteredList<Region> navigationMenu() {
return new FilteredList<>(FXCollections.observableArrayList(
caption("GENERAL"),
navLink(ThemePage.NAME, ThemePage.class),
navLink(TypographyPage.NAME, TypographyPage.class),
new Separator(),
caption("COMPONENTS"),
navLink(OverviewPage.NAME, OverviewPage.class),
navLink(InputGroupPage.NAME, InputGroupPage.class),
new Spacer(10, Orientation.VERTICAL),
navLink(AccordionPage.NAME, AccordionPage.class),
navLink(BreadcrumbsPage.NAME, BreadcrumbsPage.class),
navLink(ButtonPage.NAME, ButtonPage.class),
navLink(ChartPage.NAME, ChartPage.class),
navLink(CheckBoxPage.NAME, CheckBoxPage.class),
navLink(ColorPickerPage.NAME, ColorPickerPage.class),
navLink(ComboBoxPage.NAME, ComboBoxPage.class, "ChoiceBox"),
navLink(CustomTextFieldPage.NAME, CustomTextFieldPage.class),
navLink(DatePickerPage.NAME, DatePickerPage.class),
navLink(DialogPage.NAME, DialogPage.class),
navLink(HTMLEditorPage.NAME, HTMLEditorPage.class),
navLink(ListPage.NAME, ListPage.class),
navLink(MenuPage.NAME, MenuPage.class),
navLink(MenuButtonPage.NAME, MenuButtonPage.class, "SplitMenuButton"),
navLink(PaginationPage.NAME, PaginationPage.class),
navLink(PopoverPage.NAME, PopoverPage.class),
navLink(ProgressPage.NAME, ProgressPage.class),
navLink(RadioButtonPage.NAME, RadioButtonPage.class),
navLink(ScrollPanePage.NAME, ScrollPanePage.class),
navLink(SeparatorPage.NAME, SeparatorPage.class),
navLink(SliderPage.NAME, SliderPage.class),
navLink(SpinnerPage.NAME, SpinnerPage.class),
navLink(SplitPanePage.NAME, SplitPanePage.class),
navLink(TablePage.NAME, TablePage.class),
navLink(TabPanePage.NAME, TabPanePage.class),
navLink(TextAreaPage.NAME, TextAreaPage.class),
navLink(TextFieldPage.NAME, TextFieldPage.class, "PasswordField"),
navLink(TitledPanePage.NAME, TitledPanePage.class),
navLink(ToggleButtonPage.NAME, ToggleButtonPage.class),
navLink(ToggleSwitchPage.NAME, ToggleSwitchPage.class),
navLink(ToolBarPage.NAME, ToolBarPage.class),
navLink(TooltipPage.NAME, TooltipPage.class),
navLink(TreePage.NAME, TreePage.class),
navLink(TreeTablePage.NAME, TreeTablePage.class)
));
}
private NavLink navLink(String text, Class<? extends Page> pageClass, String... keywords) {
return navLink(text, pageClass, false, keywords);
}
@SuppressWarnings("SameParameterValue")
private NavLink navLink(String text, Class<? extends Page> pageClass, boolean isNew, String... keywords) {
var link = new NavLink(text, pageClass, isNew);
if (keywords != null && keywords.length > 0) {
link.getSearchKeywords().addAll(Arrays.asList(keywords));
}
link.setOnMouseClicked(e -> {
if (e.getSource() instanceof NavLink dest) { navigate(dest); }
});
return link;
}
private void navigate(NavLink link) {
if (selectedLink.get() != null) { selectedLink.get().pseudoClassStateChanged(SELECTED, false); }
link.pseudoClassStateChanged(SELECTED, true);
selectedLink.set(link);
}
///////////////////////////////////////////////////////////////////////////
private static class NavLink extends Label {
private final Class<? extends Page> pageClass;
private final List<String> searchKeywords = new ArrayList<>();
public NavLink(String text, Class<? extends Page> pageClass, boolean isNew) {
super(Objects.requireNonNull(text));
this.pageClass = Objects.requireNonNull(pageClass);
getStyleClass().add("nav-link");
setMaxWidth(Double.MAX_VALUE);
setContentDisplay(ContentDisplay.RIGHT);
if (isNew) {
var tag = new Label("new");
tag.getStyleClass().addAll("tag", Styles.TEXT_SMALL);
setGraphic(tag);
}
}
public Class<? extends Page> getPageClass() {
return pageClass;
}
public List<String> getSearchKeywords() {
return searchKeywords;
}
public boolean matches(String filter) {
Objects.requireNonNull(filter);
return contains(getText(), filter) || searchKeywords.stream().anyMatch(keyword -> contains(keyword, filter));
}
private boolean contains(String text, String filter) {
return text.toLowerCase().contains(filter.toLowerCase());
}
}
}

@ -0,0 +1,161 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.theme.HighlightJSTheme;
import atlantafx.sampler.theme.ThemeManager;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*;
import net.datafaker.Faker;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.sampler.util.Containers.setScrollConstraints;
public abstract class AbstractPage extends BorderPane implements Page {
protected static final int HEADER_HEIGHT = 50;
protected static final Faker FAKER = new Faker();
protected static final Random RANDOM = new Random();
protected static final EventHandler<ActionEvent> PRINT_SOURCE = System.out::println;
private static final Ikon ICON_CODE = Feather.CODE;
private static final Ikon ICON_SAMPLE = Feather.LAYOUT;
protected Button sourceCodeToggleBtn;
protected StackPane codeViewerWrapper;
protected CodeViewer codeViewer;
protected VBox userContent;
protected boolean isRendered = false;
protected AbstractPage() {
super();
getStyleClass().add("page");
createPageLayout();
}
protected void createPageLayout() {
// == header ==
var titleLabel = new Label(getName());
titleLabel.getStyleClass().addAll(Styles.TITLE_4);
codeViewer = new CodeViewer();
codeViewerWrapper = new StackPane();
codeViewerWrapper.getStyleClass().add("wrapper");
codeViewerWrapper.getChildren().setAll(codeViewer);
sourceCodeToggleBtn = new Button("", new FontIcon(ICON_CODE));
sourceCodeToggleBtn.getStyleClass().addAll(BUTTON_ICON);
sourceCodeToggleBtn.setTooltip(new Tooltip("Source Code"));
sourceCodeToggleBtn.setOnAction(e -> toggleSourceCode());
var header = new HBox();
header.getStyleClass().add("header");
header.setMinHeight(HEADER_HEIGHT);
header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().setAll(titleLabel, new Spacer(), sourceCodeToggleBtn);
// == user content ==
userContent = new VBox();
userContent.getStyleClass().add("user-content");
var userContentWrapper = new StackPane();
userContentWrapper.getStyleClass().add("wrapper");
userContentWrapper.getChildren().setAll(userContent);
var scrollPane = new ScrollPane(userContentWrapper);
setScrollConstraints(scrollPane,
ScrollPane.ScrollBarPolicy.AS_NEEDED, true,
ScrollPane.ScrollBarPolicy.AS_NEEDED, true
);
scrollPane.setMaxHeight(10_000);
// == layout ==
var stackPane = new StackPane();
stackPane.getStyleClass().add("stack");
stackPane.getChildren().addAll(codeViewerWrapper, scrollPane);
setTop(header);
setCenter(stackPane);
}
@Override
public Pane getView() {
return this;
}
@Override
public void reset() { }
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() { }
protected void toggleSourceCode() {
var graphic = (FontIcon) sourceCodeToggleBtn.getGraphic();
if (graphic.getIconCode() == ICON_SAMPLE) {
codeViewerWrapper.toBack();
graphic.setIconCode(ICON_CODE);
return;
}
var sourceFileName = getClass().getSimpleName() + ".java";
try (var stream = getClass().getResourceAsStream(sourceFileName)) {
Objects.requireNonNull(stream, "Missing source file '" + sourceFileName + "';");
// set syntax highlight theme according to JavaFX theme
var highlightJSTheme = ThemeManager.getInstance().getTheme().isDarkMode() ?
HighlightJSTheme.githubDark() :
HighlightJSTheme.githubLight();
codeViewer.setContent(stream, highlightJSTheme);
graphic.setIconCode(ICON_SAMPLE);
codeViewerWrapper.toFront();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
///////////////////////////////////////////////////////////////////////////
// Helpers //
///////////////////////////////////////////////////////////////////////////
protected <T> List<T> generate(Supplier<T> supplier, int count) {
return Stream.generate(supplier).limit(count).collect(Collectors.toList());
}
protected Feather randomIcon() {
return Feather.values()[RANDOM.nextInt(Feather.values().length)];
}
}

@ -0,0 +1,69 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import atlantafx.sampler.Resources;
import atlantafx.sampler.theme.HighlightJSTheme;
import atlantafx.sampler.util.Containers;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane;
import javafx.scene.web.WebView;
import java.io.IOException;
import java.io.InputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
public class CodeViewer extends AnchorPane {
private static final String HLJS_LIB = "assets/highlightjs/highlight.min.js";
private static final String HLJS_SCRIPT = "hljs.highlightAll();";
private WebView webView;
public CodeViewer() {
getStyleClass().add("code-viewer");
}
private void lazyLoadWebView() {
if (webView == null) {
webView = new WebView();
Containers.setAnchors(webView, Insets.EMPTY);
getChildren().setAll(webView);
}
}
@SuppressWarnings("StringBufferReplaceableByString")
public void setContent(InputStream source, HighlightJSTheme theme) {
lazyLoadWebView();
try (var hljs = Resources.getResourceAsStream(HLJS_LIB)) {
// NOTE:
// Line numbers aren't here because Highlight JS itself doesn't support it
// and highlighjs-line-numbers plugin break both indentation and colors.
webView.getEngine().loadContent(
new StringBuilder()
.append("<html>")
.append("<head>")
.append("<style>").append(theme.getCss()).append("</style>")
.append("<script>").append(new String(hljs.readAllBytes(), UTF_8)).append("</script>")
.append("<script>" + HLJS_SCRIPT + "</script>")
.append("</head>")
// Transparent background is allowed starting from OpenJFX 18.
// https://bugs.openjdk.org/browse/JDK-8090547
// Until that it should match Highlight JS background.
.append(String.format("<body style=\"background-color:%s;\">", theme.getBackground()))
.append("<pre>")
.append("<code class=\"language-java\">")
.append(new String(source.readAllBytes(), UTF_8))
.append("</code>")
.append("</pre>")
.append("</body>")
.append("</html>")
.toString()
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@ -0,0 +1,13 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import javafx.scene.Parent;
public interface Page {
String getName();
Parent getView();
void reset();
}

@ -0,0 +1,42 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class SampleBlock {
protected final VBox root;
protected final Label titleLabel;
protected final Node content;
public SampleBlock(String title, Node content) {
this.titleLabel = new Label(title);
this.titleLabel.getStyleClass().add("title");
this.content = content;
VBox.setVgrow(content, Priority.ALWAYS);
this.root = new VBox(titleLabel, content);
this.root.getStyleClass().add("sample-block");
}
public Pane getRoot() {
return root;
}
public String getText() {
return titleLabel.getText();
}
public void setText(String text) {
titleLabel.setText(text);
}
public Node getContent() {
return content;
}
}

@ -0,0 +1,103 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.AbstractPage;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Accordion;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TitledPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class AccordionPage extends AbstractPage {
public static final String NAME = "Accordion";
@Override
public String getName() { return NAME; }
private final BooleanProperty expandedProperty = new SimpleBooleanProperty(true);
private final BooleanProperty animatedProperty = new SimpleBooleanProperty(true);
public AccordionPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(new VBox(10,
controls(),
playground()
));
}
private Accordion playground() {
var textBlockContent = new Label(FAKER.chuckNorris().fact());
var textBlock = new TitledPane("_Quote", textBlockContent);
textBlock.setMnemonicParsing(true);
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);
disabledBlock.setDisable(true);
var imageBlock = new TitledPane("_Image", new VBox(10,
new ImageView(new Image(Resources.getResourceAsStream("images/20_min_adventure.jpg"))),
new TextFlow(new Text(FAKER.rickAndMorty().quote()))
));
imageBlock.animatedProperty().bind(animatedProperty);
imageBlock.setMnemonicParsing(true);
// ~
var accordion = new Accordion(
textBlock,
scrollableTextBlock,
disabledBlock,
imageBlock
);
accordion.expandedPaneProperty().addListener((obs, old, val) -> {
// make sure the accordion can never be completely collapsed
boolean hasExpanded = accordion.getPanes().stream().anyMatch(TitledPane::isExpanded);
if (expandedProperty.get() && !hasExpanded && old != null) {
Platform.runLater(() -> accordion.setExpandedPane(old));
}
});
accordion.setExpandedPane(accordion.getPanes().get(0));
return accordion;
}
private HBox controls() {
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 controls = new HBox(20, animatedToggle, expandedToggle);
controls.setPadding(new Insets(0, 0, 0, 2));
return controls;
}
}

@ -0,0 +1,97 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.Breadcrumbs;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox;
import javafx.util.Callback;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class BreadcrumbsPage extends AbstractPage {
public static final String NAME = "Breadcrumbs";
@Override
public String getName() { return NAME; }
public BreadcrumbsPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(
defaultSample().getRoot(),
customCrumbSample().getRoot()
);
}
private SampleBlock defaultSample() {
return new SampleBlock("Basic", breadcrumbs(null));
}
private SampleBlock customCrumbSample() {
Callback<TreeItem<String>, Button> crumbFactory = crumb -> {
var btn = new Button(crumb.getValue());
btn.getStyleClass().add(Styles.FLAT);
btn.setFocusTraversable(false);
if (!crumb.getChildren().isEmpty()) {
btn.setGraphic(new FontIcon(Feather.CHEVRON_RIGHT));
}
btn.setContentDisplay(ContentDisplay.RIGHT);
btn.setStyle("-fx-padding: 12px 6px 12px 6px;");
return btn;
};
return new SampleBlock("Custom crumb factory", breadcrumbs(crumbFactory));
}
private HBox breadcrumbs(Callback<TreeItem<String>, Button> crumbFactory) {
int count = 5;
TreeItem<String> model = Breadcrumbs.buildTreeModel(
generate(() -> FAKER.science().element(), count).toArray(String[]::new)
);
var nextBtn = new Button("Next");
nextBtn.getStyleClass().add(Styles.ACCENT);
var breadcrumbs = new Breadcrumbs<>(model);
breadcrumbs.setSelectedCrumb(getAncestor(model, count / 2));
if (crumbFactory != null) { breadcrumbs.setCrumbFactory(crumbFactory); }
nextBtn.setOnAction(e -> {
TreeItem<String> selected = breadcrumbs.getSelectedCrumb();
if (selected.getChildren().size() > 0) {
breadcrumbs.setSelectedCrumb(selected.getChildren().get(0));
}
});
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
if (val != null) {
nextBtn.setDisable(val.getChildren().isEmpty());
}
});
var box = new HBox(60, nextBtn, breadcrumbs);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
private <T> TreeItem<T> getAncestor(TreeItem<T> node, int height) {
var counter = height;
var current = node;
while (counter > 0 && current.getParent() != null) {
current = current.getParent();
counter--;
}
return current;
}
}

@ -0,0 +1,198 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.shape.Circle;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import static atlantafx.base.theme.Styles.*;
public class ButtonPage extends AbstractPage {
public static final String NAME = "Button";
@Override
public String getName() {
return NAME;
}
public ButtonPage() {
super();
createView();
}
private void createView() {
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(40);
grid.add(basicSamples().getRoot(), 0, 0);
grid.add(iconOnlySamples().getRoot(), 1, 0);
grid.add(coloredSamples().getRoot(), 0, 1);
grid.add(circularButtons().getRoot(), 1, 1);
grid.add(outlinedSamples().getRoot(), 0, 2);
grid.add(disabledSample().getRoot(), 1, 2);
userContent.getChildren().addAll(grid);
}
private SampleBlock basicSamples() {
var basicBtn = new Button("_Basic");
basicBtn.setMnemonicParsing(true);
basicBtn.setOnAction(PRINT_SOURCE);
var defaultBtn = new Button("_Default");
defaultBtn.setDefaultButton(true);
defaultBtn.setMnemonicParsing(true);
defaultBtn.setOnAction(PRINT_SOURCE);
var flatBtn = new Button("_Flat");
flatBtn.getStyleClass().add(FLAT);
flatBtn.setOnAction(PRINT_SOURCE);
var content = new HBox(10);
content.getChildren().addAll(basicBtn, defaultBtn, flatBtn);
return new SampleBlock("Basic", content);
}
private SampleBlock coloredSamples() {
var accentBtn = new Button("_Accent");
accentBtn.getStyleClass().add(ACCENT);
accentBtn.setMnemonicParsing(true);
accentBtn.setOnAction(PRINT_SOURCE);
var successBtn = new Button("_Success", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().add(SUCCESS);
successBtn.setMnemonicParsing(true);
successBtn.setOnAction(PRINT_SOURCE);
var dangerBtn = new Button("Da_nger", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().add(DANGER);
dangerBtn.setContentDisplay(ContentDisplay.RIGHT);
dangerBtn.setMnemonicParsing(true);
dangerBtn.setOnAction(PRINT_SOURCE);
var content = new HBox(10);
content.getChildren().addAll(accentBtn, successBtn, dangerBtn);
return new SampleBlock("Colored", content);
}
private SampleBlock iconOnlySamples() {
var basicBtn = new Button("", new FontIcon(Feather.MORE_HORIZONTAL));
basicBtn.getStyleClass().addAll(BUTTON_ICON);
var accentBtn = new Button("", new FontIcon(Feather.MENU));
accentBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
var successBtn = new Button("", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().addAll(BUTTON_ICON, SUCCESS);
var dangerBtn = new Button("", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().addAll(BUTTON_ICON, BUTTON_OUTLINED, DANGER);
var flatAccentBtn = new Button("", new FontIcon(Feather.MIC));
flatAccentBtn.getStyleClass().addAll(BUTTON_ICON, FLAT, ACCENT);
var flatSuccessBtn = new Button("", new FontIcon(Feather.USER));
flatSuccessBtn.getStyleClass().addAll(BUTTON_ICON, FLAT, SUCCESS);
var flatDangerBtn = new Button("", new FontIcon(Feather.CROSSHAIR));
flatDangerBtn.getStyleClass().addAll(BUTTON_ICON, FLAT, DANGER);
var content = new HBox(10);
content.getChildren().addAll(basicBtn, accentBtn, successBtn, dangerBtn,
flatAccentBtn, flatSuccessBtn, flatDangerBtn
);
return new SampleBlock("Icon only", content);
}
private SampleBlock circularButtons() {
var basicBtn = new Button("", new FontIcon(Feather.MORE_HORIZONTAL));
basicBtn.getStyleClass().addAll(BUTTON_CIRCLE);
basicBtn.setShape(new Circle(50));
var accentBtn = new Button("", new FontIcon(Feather.MENU));
accentBtn.getStyleClass().addAll(BUTTON_CIRCLE, ACCENT);
accentBtn.setShape(new Circle(50));
var successBtn = new Button("", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().addAll(BUTTON_CIRCLE, SUCCESS);
successBtn.setShape(new Circle(50));
var dangerBtn = new Button("", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().addAll(BUTTON_CIRCLE, BUTTON_OUTLINED, DANGER);
dangerBtn.setShape(new Circle(50));
var flatAccentBtn = new Button("", new FontIcon(Feather.MIC));
flatAccentBtn.getStyleClass().addAll(BUTTON_CIRCLE, FLAT, ACCENT);
flatAccentBtn.setShape(new Circle(50));
var flatSuccessBtn = new Button("", new FontIcon(Feather.USER));
flatSuccessBtn.getStyleClass().addAll(BUTTON_CIRCLE, FLAT, SUCCESS);
flatSuccessBtn.setShape(new Circle(50));
var flatDangerBtn = new Button("", new FontIcon(Feather.CROSSHAIR));
flatDangerBtn.getStyleClass().addAll(BUTTON_CIRCLE, FLAT, DANGER);
flatDangerBtn.setShape(new Circle(50));
var content = new HBox(10);
content.getChildren().addAll(basicBtn, accentBtn, successBtn, dangerBtn,
flatAccentBtn, flatSuccessBtn, flatDangerBtn
);
return new SampleBlock("Circular", content);
}
private SampleBlock outlinedSamples() {
var accentBtn = new Button("Accen_t");
accentBtn.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
accentBtn.setMnemonicParsing(true);
accentBtn.setOnAction(PRINT_SOURCE);
var successBtn = new Button("S_uccess", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().addAll(BUTTON_OUTLINED, SUCCESS);
successBtn.setMnemonicParsing(true);
successBtn.setOnAction(PRINT_SOURCE);
var dangerBtn = new Button("Dan_ger", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER);
dangerBtn.setContentDisplay(ContentDisplay.RIGHT);
dangerBtn.setMnemonicParsing(true);
dangerBtn.setOnAction(PRINT_SOURCE);
var content = new HBox(10);
content.getChildren().addAll(accentBtn, successBtn, dangerBtn);
return new SampleBlock("Outlined", content);
}
private SampleBlock disabledSample() {
var basicBtn = new Button("Basic");
basicBtn.setDisable(true);
var defaultBtn = new Button("Default");
defaultBtn.setDefaultButton(true);
defaultBtn.setDisable(true);
var flatBtn = new Button("Flat");
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(10);
content.getChildren().addAll(basicBtn, defaultBtn, flatBtn, iconBtn);
return new SampleBlock("Disabled", content);
}
}

@ -0,0 +1,327 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.chart.*;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import java.time.Month;
import java.time.format.TextStyle;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream;
public class ChartPage extends AbstractPage {
public static final String NAME = "Chart";
@Override
public String getName() { return NAME; }
private VBox playground;
private ComboBox<Example> exampleSelect;
public ChartPage() {
super();
createView();
}
private void createView() {
playground = new VBox(10);
playground.setMinHeight(100);
// === SELECT ===
exampleSelect = new ComboBox<>();
exampleSelect.setMaxWidth(Double.MAX_VALUE);
exampleSelect.getItems().setAll(Example.values());
exampleSelect.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val == null) { return; }
if (playground.getChildren().size() != 5) {
throw new RuntimeException("Unexpected container size.");
}
Chart newChart = createChart(val);
// copy existing properties to the new chart
findDisplayedChart().ifPresent(ch -> newChart.setDisable(ch.isDisable()));
playground.getChildren().set(2, 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);
}
});
// === CONTROLS ===
var disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> findDisplayedChart().ifPresent(ch -> {
if (val != null) { ch.setDisable(val); }
}));
var controls = new HBox(20,
new Spacer(),
disableToggle,
new Spacer()
);
controls.setAlignment(Pos.CENTER);
// ~
playground.getChildren().setAll(
new Label("Select an example:"),
exampleSelect,
new Spacer(Orientation.VERTICAL),
new Separator(),
controls
);
userContent.getChildren().setAll(playground);
}
@Override
protected void onRendered() {
super.onRendered();
exampleSelect.getSelectionModel().selectFirst();
}
private Optional<Chart> findDisplayedChart() {
return playground.getChildren().stream()
.filter(c -> c instanceof Chart)
.findFirst()
.map(c -> (Chart) c);
}
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")
private Chart areaChart(boolean stacked) {
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 = stacked ? new StackedAreaChart<>(x, y) : new AreaChart<>(x, y);
chart.setTitle("Temperature Monitoring");
chart.getData().addAll(april, may);
return chart;
}
@SuppressWarnings("unchecked")
private Chart barChart(boolean stacked) {
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 = stacked ? new StackedBarChart<>(x, y) : new BarChart<>(x, y);
chart.setTitle("Country Summary");
chart.getData().addAll(january, february, march);
return chart;
}
@SuppressWarnings("unchecked")
private Chart bubbleChart() {
final var rnd = FAKER.random();
var x = new NumberAxis(1, 53, 4);
x.setLabel("Week");
var y = new NumberAxis(0, 80, 10);
y.setLabel("Product Budget");
var series1 = new XYChart.Series<Number, Number>();
series1.setName(FAKER.commerce().productName());
IntStream.range(1, 10).forEach(i -> series1.getData().add(
new XYChart.Data<>(rnd.nextInt(1, 53), rnd.nextInt(10, 80), rnd.nextDouble(1, 10))
));
var series2 = new XYChart.Series<Number, Number>();
series2.setName(FAKER.commerce().productName());
IntStream.range(1, 10).forEach(i -> series2.getData().add(
new XYChart.Data<>(rnd.nextInt(1, 53), rnd.nextInt(10, 80), rnd.nextDouble(1, 10))
));
var chart = new BubbleChart<>(x, y);
chart.setTitle("Budget Monitoring");
chart.getData().addAll(series1, series2);
return chart;
}
@SuppressWarnings("unchecked")
private Chart lineChart() {
final var rnd = FAKER.random();
var x = new CategoryAxis();
x.setLabel("Month");
var y = new NumberAxis(0, 80, 10);
y.setLabel("Value");
var series1 = new XYChart.Series<String, Number>();
series1.setName(FAKER.stock().nsdqSymbol());
IntStream.range(1, 12).forEach(i -> series1.getData().add(
new XYChart.Data<>(Month.of(i).getDisplayName(TextStyle.SHORT, Locale.getDefault()), rnd.nextInt(10, 80))
));
var series2 = new XYChart.Series<String, Number>();
series2.setName(FAKER.stock().nsdqSymbol());
IntStream.range(1, 12).forEach(i -> series2.getData().add(
new XYChart.Data<>(Month.of(i).getDisplayName(TextStyle.SHORT, Locale.getDefault()), rnd.nextInt(10, 80))
));
var chart = new LineChart<>(x, y);
chart.setTitle("Stock Monitoring");
chart.getData().addAll(series1, series2);
return chart;
}
private Chart pieChart() {
final var rnd = FAKER.random();
ObservableList<PieChart.Data> data = FXCollections.observableArrayList(
new PieChart.Data(FAKER.food().fruit(), rnd.nextInt(10, 30)),
new PieChart.Data(FAKER.food().fruit(), rnd.nextInt(10, 30)),
new PieChart.Data(FAKER.food().fruit(), rnd.nextInt(10, 30)),
new PieChart.Data(FAKER.food().fruit(), rnd.nextInt(10, 30)),
new PieChart.Data(FAKER.food().fruit(), rnd.nextInt(10, 30))
);
var chart = new PieChart(data);
chart.setTitle("Imported Fruits");
return chart;
}
@SuppressWarnings("unchecked")
private Chart scatterChart() {
final var rnd = FAKER.random();
var x = new NumberAxis(0, 10, 1);
x.setLabel("Age");
var y = new NumberAxis(-100, 500, 100);
y.setLabel("Returns to date");
var series1 = new XYChart.Series<Number, Number>();
series1.setName("Equities");
IntStream.range(1, 10).forEach(i -> series1.getData().add(
new XYChart.Data<>(rnd.nextDouble(0, 10), rnd.nextDouble(-100, 500))
));
var series2 = new XYChart.Series<Number, Number>();
series2.setName("Mutual funds");
IntStream.range(1, 10).forEach(i -> series2.getData().add(
new XYChart.Data<>(rnd.nextDouble(0, 10), rnd.nextDouble(-100, 500))
));
var chart = new ScatterChart<>(x, y);
chart.setTitle("Investment Overview");
chart.getData().addAll(series1, series2);
return chart;
}
private enum Example {
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);
}
}
}

@ -0,0 +1,92 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
public class CheckBoxPage extends AbstractPage {
public static final String NAME = "CheckBox";
@Override
public String getName() { return NAME; }
private CheckBox basicCheck;
private CheckBox indeterminateCheck;
public CheckBoxPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(
basicSamples(),
disabledSamples()
);
}
private HBox basicSamples() {
basicCheck = new CheckBox("_Check Me");
basicCheck.setMnemonicParsing(true);
basicCheck.setOnAction(PRINT_SOURCE);
var basicBlock = new SampleBlock("Basic", basicCheck);
indeterminateCheck = new CheckBox("C_heck Me");
indeterminateCheck.setAllowIndeterminate(true);
indeterminateCheck.setIndeterminate(true);
indeterminateCheck.setMnemonicParsing(true);
indeterminateCheck.setOnAction(PRINT_SOURCE);
var indeterminateBlock = new SampleBlock("Indeterminate", indeterminateCheck);
var root = new HBox(20);
root.getChildren().addAll(
basicBlock.getRoot(),
indeterminateBlock.getRoot()
);
return root;
}
private HBox disabledSamples() {
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);
var disabledBlock = new SampleBlock("Disabled", new HBox(10, basicCheck, indeterminateCheck));
var root = new HBox(20);
root.getChildren().addAll(disabledBlock.getRoot());
return root;
}
// visually compare normal and indeterminate checkboxes size
protected void onRendered() {
var normalBox = basicCheck.lookup(".box");
var indeterminateBox = indeterminateCheck.lookup(".box");
if (normalBox == null || indeterminateBox == null) { return; }
// force layout to obtain node bounds
((StackPane) normalBox).layout();
((StackPane) indeterminateBox).layout();
basicCheck.setText(String.format("_Check Me (size = H%.2f x W%.2f)",
normalBox.getBoundsInParent().getHeight(),
normalBox.getBoundsInParent().getWidth()
));
indeterminateCheck.setText(String.format("C_heck Me (box size = H%.2f x W%.2f)",
normalBox.getBoundsInParent().getHeight(),
normalBox.getBoundsInParent().getWidth()
));
}
}

@ -0,0 +1,97 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
import javafx.geometry.Pos;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
public class ColorPickerPage extends AbstractPage {
public static final String NAME = "ColorPicker";
@Override
public String getName() { return NAME; }
public ColorPickerPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(
playground()
);
}
private VBox playground() {
var colorPicker = new ColorPicker();
colorPicker.setValue(Color.AQUA);
var pickerBox = new HBox(
new Spacer(),
colorPicker,
new Spacer()
);
var labelToggle = new ToggleSwitch("Show label");
labelToggle.setSelected(true);
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("Disable");
colorPicker.disableProperty().bind(disableToggle.selectedProperty());
var pickerStyleBox = new HBox(5, new Label("Picker Style"), pickerStyleChoice(colorPicker));
pickerStyleBox.setAlignment(Pos.CENTER);
var controls = new HBox(20,
new Spacer(),
pickerStyleBox,
labelToggle,
disableToggle,
new Spacer()
);
controls.setAlignment(Pos.CENTER);
// ~
var root = new VBox(20);
root.setAlignment(Pos.CENTER);
root.getChildren().setAll(
pickerBox,
new Separator(),
controls
);
return root;
}
private ChoiceBox<String> pickerStyleChoice(ColorPicker colorPicker) {
var optDefault = "Default";
var optButton = "Button";
var optSplitButton = "Split Button";
var choice = new ChoiceBox<String>();
choice.getItems().setAll(optDefault, optButton, optSplitButton);
choice.setPrefWidth(120);
choice.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val == null) { return; }
colorPicker.getStyleClass().removeAll(ColorPicker.STYLE_CLASS_BUTTON, ColorPicker.STYLE_CLASS_SPLIT_BUTTON);
if (optButton.equals(val)) { colorPicker.getStyleClass().add(ColorPicker.STYLE_CLASS_BUTTON); }
if (optSplitButton.equals(val)) { colorPicker.getStyleClass().add(ColorPicker.STYLE_CLASS_SPLIT_BUTTON); }
});
choice.getSelectionModel().select(optDefault);
return choice;
}
}

@ -0,0 +1,199 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static atlantafx.base.theme.Styles.STATE_DANGER;
import static atlantafx.base.theme.Styles.STATE_SUCCESS;
import static atlantafx.sampler.util.Containers.H_GROW_NEVER;
import static javafx.collections.FXCollections.observableArrayList;
public class ComboBoxPage extends AbstractPage {
public static final String NAME = "ComboBox";
private static final int PREF_WIDTH = 200;
@Override
public String getName() { return NAME; }
public ComboBoxPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(
createGrid()
);
}
private Pane createGrid() {
var grid = new GridPane();
grid.setHgap(20);
grid.setVgap(10);
grid.getColumnConstraints().setAll(H_GROW_NEVER, H_GROW_NEVER, H_GROW_NEVER);
grid.setMaxWidth((PREF_WIDTH * 3) + 100);
var comboLabel = new Label("C_omboBox");
comboLabel.setMnemonicParsing(true);
comboLabel.setStyle("-fx-font-weight: bold;");
grid.add(comboLabel, 0, 0);
var choiceLabel = new Label("C_hoiceBox");
choiceLabel.setMnemonicParsing(true);
choiceLabel.setStyle("-fx-font-weight: bold;");
grid.add(choiceLabel, 2, 0);
// default
grid.add(comboBox(), 0, 1);
grid.add(label("empty"), 1, 1);
grid.add(choiceBox(), 2, 1);
// editable
grid.add(comboBox(c -> {
c.setItems(createItems(5));
c.setEditable(true);
}), 0, 2);
grid.add(label("editable"), 1, 2);
// placeholder
grid.add(comboBox(c -> c.setPlaceholder(new Label("Loading..."))), 0, 3);
grid.add(label("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(label("graphic"), 1, 4);
// success
grid.add(comboBox(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_SUCCESS, true);
c.getSelectionModel().selectFirst();
}), 0, 5);
grid.add(label("success"), 1, 5);
grid.add(choiceBox(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_SUCCESS, true);
c.getSelectionModel().selectFirst();
}), 2, 5);
// negative
grid.add(comboBox(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_DANGER, true);
c.getSelectionModel().selectFirst();
}), 0, 6);
grid.add(label("success"), 1, 6);
grid.add(choiceBox(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_DANGER, true);
c.getSelectionModel().selectFirst();
}), 2, 6);
// disabled
grid.add(comboBox(c -> c.setDisable(true)), 0, 7);
grid.add(label("disabled"), 1, 7);
grid.add(choiceBox(c -> c.setDisable(true)), 2, 7);
// overflow
grid.add(comboBox(c -> {
c.setItems(createItems(50));
c.getSelectionModel().selectFirst();
}), 0, 8);
grid.add(label("large list"), 1, 8);
grid.add(choiceBox(c -> {
c.setItems(createItems(50));
c.getSelectionModel().selectFirst();
}), 2, 8);
// overflow
grid.add(comboBox(c -> {
c.setItems(observableArrayList(generate(() -> FAKER.chuckNorris().fact(), 5)));
c.getSelectionModel().selectFirst();
}), 0, 9);
grid.add(label("wide text"), 1, 9);
grid.add(choiceBox(c -> {
c.setItems(observableArrayList(generate(() -> FAKER.chuckNorris().fact(), 5)));
c.getSelectionModel().selectFirst();
}), 2, 9);
return grid;
}
private ObservableList<String> createItems(int count) {
return observableArrayList(generate(() -> FAKER.hipster().word(), count));
}
private Label label(String text) {
return new Label(text) {{
GridPane.setHalignment(this, HPos.CENTER);
}};
}
private ComboBox<String> comboBox() {
return comboBox(null);
}
private ComboBox<String> comboBox(Consumer<ComboBox<String>> mutator) {
var c = new ComboBox<String>();
c.setPrefWidth(PREF_WIDTH);
if (mutator != null) { mutator.accept(c); }
return c;
}
private ChoiceBox<String> choiceBox() {
return choiceBox(null);
}
private ChoiceBox<String> choiceBox(Consumer<ChoiceBox<String>> mutator) {
var c = new ChoiceBox<String>();
c.setPrefWidth(PREF_WIDTH);
if (mutator != null) { mutator.accept(c); }
return c;
}
///////////////////////////////////////////////////////////////////////////
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());
}
}
}
}

@ -0,0 +1,71 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.CustomTextField;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.layout.FlowPane;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import static atlantafx.base.theme.Styles.STATE_DANGER;
import static atlantafx.base.theme.Styles.STATE_SUCCESS;
public class CustomTextFieldPage extends AbstractPage {
public static final String NAME = "CustomTextField";
@Override
public String getName() { return NAME; }
public CustomTextFieldPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(samples());
}
private FlowPane samples() {
var leftIconField = new CustomTextField();
leftIconField.setPromptText("Prompt text");
leftIconField.setRight(new FontIcon(Feather.X));
var leftIconBlock = new SampleBlock("Node on the left", leftIconField);
var rightIconField = new CustomTextField();
rightIconField.setPromptText("Prompt text");
rightIconField.setLeft(new FontIcon(Feather.MAP_PIN));
var rightIconBlock = new SampleBlock("Node on the right", rightIconField);
var bothIconField = new CustomTextField("Text");
bothIconField.setLeft(new FontIcon(Feather.MAP_PIN));
bothIconField.setRight(new FontIcon(Feather.X));
var bothIconBlock = new SampleBlock("Nodes on both sides", bothIconField);
var noSideIconsField = new CustomTextField("Text");
var noSideIconsBlock = new SampleBlock("No side nodes", noSideIconsField);
var successField = new CustomTextField("Text");
successField.pseudoClassStateChanged(STATE_SUCCESS, true);
successField.setRight(new FontIcon(Feather.X));
var successBlock = new SampleBlock("Success", successField);
var dangerField = new CustomTextField("Text");
dangerField.pseudoClassStateChanged(STATE_DANGER, true);
dangerField.setLeft(new FontIcon(Feather.MAP_PIN));
var dangerBlock = new SampleBlock("Danger", dangerField);
var flowPane = new FlowPane(20, 20);
flowPane.getChildren().setAll(
leftIconBlock.getRoot(),
rightIconBlock.getRoot(),
bothIconBlock.getRoot(),
noSideIconsBlock.getRoot(),
successBlock.getRoot(),
dangerBlock.getRoot()
);
return flowPane;
}
}

@ -0,0 +1,175 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.control.DateCell;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import javafx.util.StringConverter;
import java.time.LocalDate;
import java.time.chrono.HijrahChronology;
import java.time.format.DateTimeFormatter;
import static javafx.scene.layout.GridPane.REMAINING;
public class DatePickerPage extends AbstractPage {
public static final String NAME = "DatePicker";
private static final LocalDate TODAY = LocalDate.now();
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_DATE;
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;
@Override
public String getName() { return NAME; }
private final BooleanProperty weekNumProperty = new SimpleBooleanProperty();
private final BooleanProperty editableProperty = new SimpleBooleanProperty();
private final BooleanProperty offPastDatesProperty = new SimpleBooleanProperty();
private final BooleanProperty disableProperty = new SimpleBooleanProperty();
public DatePickerPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(playground());
}
private GridPane playground() {
var playground = new GridPane();
playground.setHgap(40);
playground.setVgap(10);
final var popupDatePicker = popupDatePicker();
final var inlineDatePicker = inlineDatePicker(null);
var inlineValue = new Label();
inlineValue.setAlignment(Pos.CENTER);
inlineValue.setMaxWidth(Double.MAX_VALUE);
inlineValue.textProperty().bind(inlineDatePicker.valueProperty().asString());
// == CONTROLS ==
var weekNumToggle = new ToggleSwitch("Week numbers");
weekNumProperty.bind(weekNumToggle.selectedProperty());
weekNumToggle.setSelected(true);
var chronologyToggle = new ToggleSwitch("Second chronology");
chronologyToggle.selectedProperty().addListener((obs, old, val) ->
popupDatePicker.setChronology(val ? HijrahChronology.INSTANCE : null)
);
var editableToggle = new ToggleSwitch("Editable");
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 CustomDateCell() : null);
popupDatePicker.setValue(TODAY);
// we have to create new date picker, because changing cell factory won't update existing cells
var datePicker = inlineDatePicker(val ? dp -> new CustomDateCell() : null);
playground.getChildren().removeIf(n -> n instanceof InlineDatePicker);
playground.add(datePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW);
inlineValue.textProperty().unbind();
inlineValue.textProperty().bind(datePicker.valueProperty().asString());
});
var disablePickerToggle = new ToggleSwitch("Disable");
disableProperty.bind(disablePickerToggle.selectedProperty());
var controls = new VBox(10,
weekNumToggle,
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);
playground.add(defaultLabel, 0, 0);
playground.add(popupDatePicker, 0, 1);
playground.add(new Spacer(20), 0, 2);
playground.add(inlineLabel, 0, 3);
playground.add(inlineDatePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW);
playground.add(inlineValue, 0, 5);
playground.add(controls, 1, 0, 1, REMAINING);
return playground;
}
private DatePicker popupDatePicker() {
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 inlineDatePicker(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);
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 CustomDateCell extends DateCell {
public void updateItem(LocalDate date, boolean empty) {
super.updateItem(date, empty);
setDisable(empty || date.compareTo(TODAY) < 0);
}
}
}

@ -0,0 +1,230 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.StageStyle;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import static javafx.scene.control.Alert.AlertType;
import static javafx.scene.control.ButtonBar.ButtonData;
public class DialogPage extends AbstractPage {
public static final String NAME = "Dialog";
@Override
public String getName() { return NAME; }
private final BooleanProperty showHeaderProperty = new SimpleBooleanProperty(true);
private final BooleanProperty minDecorationsProperty = new SimpleBooleanProperty(true);
public DialogPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(playground());
}
private VBox playground() {
var showHeaderToggle = new ToggleSwitch("Show header");
showHeaderProperty.bind(showHeaderToggle.selectedProperty());
showHeaderToggle.setSelected(true);
var minDecorationsToggle = new ToggleSwitch("Minimum decorations");
minDecorationsProperty.bind(minDecorationsToggle.selectedProperty());
minDecorationsToggle.setSelected(true);
var controls = new HBox(20, new Spacer(), showHeaderToggle, minDecorationsToggle, new Spacer());
controls.setAlignment(Pos.CENTER);
// ~
var row1 = new HBox(40, infoDialogButton().getRoot(), warnDialogButton().getRoot(), errorDialogButton().getRoot());
var row2 = new HBox(40, exceptionDialogButton().getRoot(), confirmationDialogButton().getRoot(), textInputDialogButton().getRoot(), choiceDialogButton().getRoot());
var playground = new VBox(20);
playground.setMinHeight(100);
playground.getChildren().setAll(controls, new Separator(Orientation.HORIZONTAL), row1, row2);
return playground;
}
private SampleBlock infoDialogButton() {
var button = new Button("Click");
button.setGraphic(new FontIcon(Feather.INFO));
button.setOnAction(e -> {
var alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Information Dialog");
alert.setHeaderText(randomHeader());
alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait();
});
return new SampleBlock("Information", button);
}
private SampleBlock warnDialogButton() {
var button = new Button("Click");
button.setGraphic(new FontIcon(Feather.ALERT_TRIANGLE));
button.setOnAction(e -> {
var alert = new Alert(AlertType.WARNING);
alert.setTitle("Warning Dialog");
alert.setHeaderText(randomHeader());
alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait();
});
return new SampleBlock("Warning", button);
}
private SampleBlock errorDialogButton() {
var button = new Button("Click");
button.setGraphic(new FontIcon(Feather.X_CIRCLE));
button.setOnAction(e -> {
var alert = new Alert(AlertType.ERROR);
alert.setTitle("Error Dialog");
alert.setHeaderText(randomHeader());
alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait();
});
return new SampleBlock("Error", button);
}
private SampleBlock exceptionDialogButton() {
var button = new Button("Click");
button.setGraphic(new FontIcon(Feather.MEH));
button.setOnAction(e -> {
var alert = new Alert(AlertType.ERROR);
alert.setTitle("Error Dialog");
alert.setHeaderText(randomHeader());
alert.setContentText(FAKER.lorem().paragraph(3));
var exception = new RuntimeException(FAKER.chuckNorris().fact());
var stringWriter = new StringWriter();
var printWriter = new PrintWriter(stringWriter);
exception.printStackTrace(printWriter);
var label = new Label("Full stacktrace:");
var textArea = new TextArea(stringWriter.toString());
textArea.setEditable(false);
textArea.setWrapText(false);
textArea.setMaxWidth(Double.MAX_VALUE);
textArea.setMaxHeight(Double.MAX_VALUE);
GridPane.setVgrow(textArea, Priority.ALWAYS);
GridPane.setHgrow(textArea, Priority.ALWAYS);
var content = new GridPane();
content.setMaxWidth(Double.MAX_VALUE);
content.add(label, 0, 0);
content.add(textArea, 0, 1);
alert.getDialogPane().setExpandableContent(content);
alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait();
});
return new SampleBlock("Exception", button);
}
private SampleBlock confirmationDialogButton() {
var button = new Button("Click");
button.setGraphic(new FontIcon(Feather.CHECK_SQUARE));
button.setOnAction(e -> {
var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Confirmation Dialog");
alert.setHeaderText(randomHeader());
alert.setContentText(FAKER.lorem().paragraph(3));
ButtonType yesBtn = new ButtonType("Yes", ButtonData.YES);
ButtonType noBtn = new ButtonType("No", ButtonData.NO);
ButtonType cancelBtn = new ButtonType("Cancel", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(yesBtn, noBtn, cancelBtn);
alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait();
});
return new SampleBlock("Confirmation", button);
}
private SampleBlock textInputDialogButton() {
var button = new Button("Click");
button.setGraphic(new FontIcon(Feather.EDIT_2));
button.setOnAction(e -> {
var dialog = new TextInputDialog();
dialog.setTitle("Text Input Dialog");
dialog.setHeaderText(randomHeader());
dialog.setContentText("Enter your name:");
dialog.initOwner(getScene().getWindow());
dialog.initStyle(getModality());
dialog.showAndWait();
});
return new SampleBlock("Text Input", button);
}
private SampleBlock choiceDialogButton() {
var button = new Button("Click");
button.setGraphic(new FontIcon(Feather.LIST));
button.setOnAction(e -> {
var choices = new ArrayList<>();
choices.add("A");
choices.add("B");
choices.add("C");
var dialog = new ChoiceDialog<>(choices.get(0), choices);
dialog.setTitle("Choice Dialog");
dialog.setHeaderText(randomHeader());
dialog.setContentText("Choose your letter:");
dialog.initOwner(getScene().getWindow());
dialog.initStyle(getModality());
dialog.showAndWait();
});
return new SampleBlock("Choice", button);
}
private String randomHeader() {
return showHeaderProperty.get() ? FAKER.chuckNorris().fact() : null;
}
private StageStyle getModality() {
return minDecorationsProperty.get() ? StageStyle.UTILITY : StageStyle.DECORATED;
}
}

@ -0,0 +1,28 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import javafx.scene.web.HTMLEditor;
public class HTMLEditorPage extends AbstractPage {
public static final String NAME = "HTMLEditor";
public HTMLEditorPage() {
super();
createView();
}
private void createView() {
var editor = new HTMLEditor();
editor.setPrefHeight(400);
editor.setHtmlText(String.join("<br/><br/>", FAKER.lorem().paragraphs(5)));
userContent.getChildren().setAll(editor);
}
@Override
public String getName() {
return NAME;
}
}

@ -0,0 +1,104 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
public class InputGroupPage extends AbstractPage {
public static final String NAME = "Input Group";
@Override
public String getName() { return NAME; }
public InputGroupPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(new FlowPane(
20, 20,
httpMethodSample().getRoot(),
passwordSample().getRoot(),
networkSample().getRoot(),
dropdownSample().getRoot()
));
}
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 PasswordField();
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("Password Field + Button", box);
}
private SampleBlock networkSample() {
var leftText = new TextField("192.168.1.10");
leftText.getStyleClass().add(Styles.LEFT_PILL);
var centerText = new TextField("24");
centerText.getStyleClass().add(Styles.CENTER_PILL);
centerText.setPrefWidth(50);
var rightText = new TextField("192.168.1.10");
rightText.getStyleClass().add(Styles.RIGHT_PILL);
var box = new HBox(leftText, centerText, rightText);
box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Text Fields", box);
}
private SampleBlock dropdownSample() {
var leftMenu = new MenuButton(FAKER.harryPotter().character());
leftMenu.getItems().addAll(
new MenuItem(FAKER.harryPotter().spell()),
new MenuItem(FAKER.harryPotter().spell()),
new MenuItem(FAKER.harryPotter().spell())
);
leftMenu.getStyleClass().add(Styles.LEFT_PILL);
var rightText = new TextField();
rightText.getStyleClass().add(Styles.RIGHT_PILL);
var box = new HBox(leftMenu, rightText);
box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("MenuButton + TextField", box);
}
}

@ -0,0 +1,299 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.sampler.fake.domain.Book;
import atlantafx.sampler.page.AbstractPage;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.*;
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.VBox;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
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 static atlantafx.base.theme.Styles.*;
public class ListPage extends AbstractPage {
public static final String NAME = "ListView";
@Override
public String getName() { return NAME; }
private VBox playground;
private ComboBox<Example> exampleSelect;
private final List<Book> dataList = generate(() -> Book.random(FAKER), 50);
private final StringConverter<Book> bookStringConverter = new StringConverter<>() {
@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);
}
};
public ListPage() {
super();
createView();
}
private void createView() {
exampleSelect = exampleSelect();
playground = playground(exampleSelect);
userContent.getChildren().setAll(playground);
}
@Override
protected void onRendered() {
super.onRendered();
exampleSelect.getSelectionModel().selectFirst();
}
private VBox playground(ComboBox<Example> exampleSelect) {
var playground = new VBox(10);
playground.setMinHeight(100);
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 disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> findDisplayedList().ifPresent(lv -> {
if (val != null) { lv.setDisable(val); }
}));
var controls = new HBox(20,
new Spacer(),
borderedToggle,
denseToggle,
stripedToggle,
disableToggle,
new Spacer()
);
playground.getChildren().setAll(
new Label("Select an example:"),
exampleSelect,
new Spacer(Orientation.VERTICAL), // placeholder for ListView<?>
controls
);
return playground;
}
private ComboBox<Example> exampleSelect() {
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; }
if (playground.getChildren().size() != 4) {
throw new RuntimeException("Unexpected container size.");
}
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());
});
playground.getChildren().set(2, 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;
}
private Optional<ListView<?>> findDisplayedList() {
if (playground == null) { return Optional.empty(); }
return playground.getChildren().stream()
.filter(c -> c instanceof ListView<?>)
.findFirst()
.map(c -> (ListView<?>) c);
}
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 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,243 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Side;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.stream.IntStream;
import static atlantafx.base.theme.Styles.*;
public class MenuButtonPage extends AbstractPage {
public static final String NAME = "MenuButton";
@Override
public String getName() { return NAME; }
public MenuButtonPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(
basicSample().getRoot(),
coloredSample().getRoot(),
popupSideSample().getRoot(),
iconOnlySample().getRoot(),
outlinedSample().getRoot(),
disabledSample().getRoot()
);
}
private SampleBlock basicSample() {
var basicMenuBtn = new MenuButton("_Menu Button");
basicMenuBtn.getItems().setAll(menuItems(5));
basicMenuBtn.setMnemonicParsing(true);
basicMenuBtn.setOnAction(PRINT_SOURCE);
var basicSplitBtn = new SplitMenuButton(menuItems(5));
basicSplitBtn.setText("_Split Menu Button");
basicSplitBtn.setMnemonicParsing(true);
basicSplitBtn.setOnAction(PRINT_SOURCE);
var flatMenuBtn = new MenuButton("Flat");
flatMenuBtn.getItems().setAll(menuItems(5));
flatMenuBtn.getStyleClass().add(FLAT);
var flatSplitBtn = new SplitMenuButton(menuItems(5));
flatSplitBtn.setText("Flat");
flatSplitBtn.getStyleClass().add(FLAT);
var content = new HBox(10);
content.getChildren().addAll(basicMenuBtn, basicSplitBtn, flatMenuBtn, flatSplitBtn);
return new SampleBlock("Basic", content);
}
private SampleBlock coloredSample() {
var accentMenuBtn = new MenuButton("_Accent");
accentMenuBtn.getItems().setAll(menuItems(5));
accentMenuBtn.getStyleClass().add(ACCENT);
accentMenuBtn.setMnemonicParsing(true);
accentMenuBtn.setOnAction(PRINT_SOURCE);
var accentSplitBtn = new SplitMenuButton(menuItems(5));
accentSplitBtn.setText("Accent");
accentSplitBtn.getStyleClass().add(ACCENT);
var successMenuBtn = new MenuButton("Success");
successMenuBtn.getItems().setAll(menuItems(5));
successMenuBtn.setGraphic(new FontIcon(Feather.CHECK));
successMenuBtn.getStyleClass().add(SUCCESS);
var successSplitBtn = new SplitMenuButton(menuItems(5));
successMenuBtn.setGraphic(new FontIcon(Feather.CHECK));
successSplitBtn.setText("_Success");
successSplitBtn.getStyleClass().add(SUCCESS);
successSplitBtn.setMnemonicParsing(true);
successSplitBtn.setOnAction(PRINT_SOURCE);
var dangerMenuBtn = new MenuButton("Danger");
dangerMenuBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerMenuBtn.getItems().setAll(menuItems(5));
dangerMenuBtn.getStyleClass().add(DANGER);
var dangerSplitBtn = new SplitMenuButton(menuItems(5));
dangerSplitBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerSplitBtn.setText("_Danger");
dangerSplitBtn.getStyleClass().add(DANGER);
dangerSplitBtn.setMnemonicParsing(true);
dangerSplitBtn.setOnAction(PRINT_SOURCE);
var content = new HBox(10);
content.getChildren().addAll(
accentMenuBtn, accentSplitBtn,
successMenuBtn, successSplitBtn,
dangerMenuBtn, dangerSplitBtn
);
return new SampleBlock("Colored", content);
}
private SampleBlock popupSideSample() {
var topMenuBtn = new MenuButton("Top");
topMenuBtn.getItems().setAll(menuItems(5));
topMenuBtn.setPopupSide(Side.TOP);
var rightMenuBtn = new MenuButton("Right");
rightMenuBtn.getItems().setAll(menuItems(5));
rightMenuBtn.setPopupSide(Side.RIGHT);
rightMenuBtn.getStyleClass().add(ACCENT);
var bottomMenuBtn = new MenuButton("Bottom");
bottomMenuBtn.setGraphic(new FontIcon(Feather.CHECK));
bottomMenuBtn.getItems().setAll(menuItems(5));
bottomMenuBtn.setPopupSide(Side.BOTTOM);
bottomMenuBtn.getStyleClass().add(SUCCESS);
var leftMenuBtn = new MenuButton("Left");
leftMenuBtn.setGraphic(new FontIcon(Feather.TRASH));
leftMenuBtn.getItems().setAll(menuItems(5));
leftMenuBtn.setPopupSide(Side.LEFT);
leftMenuBtn.getStyleClass().add(DANGER);
var content = new FlowPane(10, 10);
content.getChildren().addAll(topMenuBtn, rightMenuBtn, bottomMenuBtn, leftMenuBtn);
return new SampleBlock("Popup Side", content);
}
private SampleBlock iconOnlySample() {
var basicMenuBtn = new MenuButton();
basicMenuBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
basicMenuBtn.getItems().setAll(menuItems(5));
basicMenuBtn.getStyleClass().addAll(BUTTON_ICON);
var basicSplitBtn = new SplitMenuButton(menuItems(5));
basicSplitBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
basicSplitBtn.getStyleClass().addAll(BUTTON_ICON);
var accentMenuBtn = new MenuButton();
accentMenuBtn.setGraphic(new FontIcon(Feather.MENU));
accentMenuBtn.getItems().setAll(menuItems(5));
accentMenuBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
var accentSplitBtn = new SplitMenuButton(menuItems(5));
accentSplitBtn.setGraphic(new FontIcon(Feather.MENU));
accentSplitBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
var content = new FlowPane(10, 10);
content.getChildren().addAll(basicMenuBtn, basicSplitBtn, accentMenuBtn, accentSplitBtn);
return new SampleBlock("Icons", content);
}
private SampleBlock outlinedSample() {
var accentMenuBtn = new MenuButton("Accen_t");
accentMenuBtn.getItems().setAll(menuItems(5));
accentMenuBtn.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
accentMenuBtn.setMnemonicParsing(true);
accentMenuBtn.setOnAction(PRINT_SOURCE);
var accentSplitBtn = new SplitMenuButton(menuItems(5));
accentSplitBtn.setText("Accent");
accentSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
var successMenuBtn = new MenuButton("S_uccess");
successMenuBtn.getItems().setAll(menuItems(5));
successMenuBtn.setGraphic(new FontIcon(Feather.CHECK));
successMenuBtn.getStyleClass().addAll(BUTTON_OUTLINED, SUCCESS);
successMenuBtn.setMnemonicParsing(true);
successMenuBtn.setOnAction(PRINT_SOURCE);
var successSplitBtn = new SplitMenuButton(menuItems(5));
successMenuBtn.setGraphic(new FontIcon(Feather.CHECK));
successSplitBtn.setText("Success");
successSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, SUCCESS);
var dangerMenuBtn = new MenuButton("Danger");
dangerMenuBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerMenuBtn.getItems().setAll(menuItems(5));
dangerMenuBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER);
var dangerSplitBtn = new SplitMenuButton(menuItems(5));
dangerSplitBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerSplitBtn.setText("Dan_ger");
dangerSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER);
dangerSplitBtn.setMnemonicParsing(true);
dangerSplitBtn.setOnAction(PRINT_SOURCE);
var content = new HBox(10);
content.getChildren().addAll(
accentMenuBtn, accentSplitBtn,
successMenuBtn, successSplitBtn,
dangerMenuBtn, dangerSplitBtn
);
return new SampleBlock("Outlined", content);
}
private SampleBlock disabledSample() {
var basicMenuBtn = new MenuButton("Menu Button");
basicMenuBtn.getItems().setAll(menuItems(5));
basicMenuBtn.setDisable(true);
var accentSplitBtn = new SplitMenuButton();
accentSplitBtn.setText("Accent");
accentSplitBtn.getItems().setAll(menuItems(5));
accentSplitBtn.getStyleClass().addAll(ACCENT);
accentSplitBtn.setDisable(true);
var flatMenuBtn = new MenuButton("Flat");
flatMenuBtn.getItems().setAll(menuItems(5));
flatMenuBtn.getStyleClass().addAll(FLAT);
flatMenuBtn.setDisable(true);
var iconMenuBtn = new MenuButton();
iconMenuBtn.getItems().setAll(menuItems(5));
iconMenuBtn.getStyleClass().addAll(BUTTON_ICON);
iconMenuBtn.setDisable(true);
var sample = new HBox(10);
sample.getChildren().addAll(basicMenuBtn, accentSplitBtn, flatMenuBtn, iconMenuBtn);
return new SampleBlock("Disabled", sample);
}
@SuppressWarnings("SameParameterValue")
private MenuItem[] menuItems(int count) {
return IntStream.range(0, count)
.mapToObj(i -> new MenuItem(FAKER.babylon5().character()))
.toArray(MenuItem[]::new);
}
}

@ -0,0 +1,72 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.fake.SampleMenuBar;
import atlantafx.sampler.page.AbstractPage;
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 org.kordamp.ikonli.feather.Feather;
import static atlantafx.sampler.util.Controls.menuItem;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
public class MenuPage extends AbstractPage {
public static final String NAME = "Menu";
@Override
public String getName() { return NAME; }
public MenuPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(
menuBarSample().getRoot(),
contextMenuExample().getRoot()
);
}
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);
undoItem.setOnAction(PRINT_SOURCE);
var redoItem = Controls.menuItem("_Redo", Feather.CORNER_DOWN_RIGHT, new KeyCodeCombination(KeyCode.Y, CONTROL_DOWN));
redoItem.setMnemonicParsing(true);
redoItem.setOnAction(PRINT_SOURCE);
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 sample = new Label("Right-Click Here");
sample.setAlignment(Pos.CENTER);
sample.setMinSize(400, 80);
sample.setMaxSize(200, 80);
sample.setContextMenu(contextMenu);
sample.getStyleClass().add(Styles.BORDERED);
return new SampleBlock("Context menu", sample);
}
}

@ -0,0 +1,327 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.util.IntegerStringConverter;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.*;
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;
import java.time.LocalDate;
import java.util.stream.IntStream;
import static atlantafx.base.theme.Styles.*;
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;
private static final int H_GAP = 20;
private static final int V_GAP = 10;
@Override
public String getName() { return NAME; }
public OverviewPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(
buttonSample().getRoot(),
new HBox(H_GAP * 2,
iconButtonSample().getRoot(),
dropdownMenuSample().getRoot()
),
new HBox(H_GAP * 2,
checkBoxSample().getRoot(),
radioButtonSample().getRoot(),
toggleSwitchSample().getRoot()
),
comboBoxSample().getRoot(),
sliderSample().getRoot(),
new HBox(H_GAP * 2,
textFieldSample().getRoot(),
spinnerSample().getRoot()
),
textAreaSample().getRoot()
);
}
private SampleBlock buttonSample() {
var grid = new GridPane();
grid.setVgap(V_GAP);
grid.setHgap(H_GAP);
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 = toggleButton("Toggle 1", twoButtonGroup, true, LEFT_PILL);
leftPill.setPrefWidth(BUTTON_WIDTH + grid.getHgap() / 2);
var rightPill = toggleButton("Toggle 2", twoButtonGroup, false, RIGHT_PILL);
rightPill.setPrefWidth(BUTTON_WIDTH + grid.getHgap() / 2);
var twoButtonBox = new HBox(leftPill, rightPill);
// ~
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 grid = new GridPane();
grid.setVgap(V_GAP);
grid.setHgap(H_GAP);
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));
// ~
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 grid = new GridPane();
grid.setVgap(V_GAP);
grid.setHgap(H_GAP);
var basicIconBtn = new MenuButton();
basicIconBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
basicIconBtn.getItems().setAll(menuItems(5));
basicIconBtn.getStyleClass().addAll(BUTTON_ICON);
var accentIconBtn = new MenuButton();
accentIconBtn.setGraphic(new FontIcon(Feather.MENU));
accentIconBtn.getItems().setAll(menuItems(5));
accentIconBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
var basicMenuBtn = new MenuButton("Menu Button");
basicMenuBtn.getItems().setAll(menuItems(5));
basicMenuBtn.setPrefWidth(COMBO_BOX_WIDTH);
var basicSplitBtn = new SplitMenuButton(menuItems(5));
basicSplitBtn.setText("Split Menu Button");
var outlinedSplitBtn = new SplitMenuButton(menuItems(5));
outlinedSplitBtn.setGraphic(new FontIcon(Feather.TRASH));
outlinedSplitBtn.setText("Danger");
outlinedSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER);
outlinedSplitBtn.setPrefWidth(COMBO_BOX_WIDTH);
// ~
grid.add(basicIconBtn, 0, 0);
grid.add(accentIconBtn, 1, 0);
grid.add(basicMenuBtn, 2, 0);
grid.add(basicSplitBtn, 0, 1, 2, 1);
grid.add(outlinedSplitBtn, 2, 1);
return new SampleBlock("Dropdown Menus", grid);
}
private SampleBlock checkBoxSample() {
var box = new VBox(V_GAP);
var opt1 = new CheckBox("Option 1");
var opt2 = new CheckBox("Option 1");
opt2.setSelected(true);
var opt3 = new CheckBox("Option 3");
opt3.setAllowIndeterminate(true);
opt3.setIndeterminate(true);
box.getChildren().setAll(opt1, opt2, opt3);
return new SampleBlock("Check Boxes", box);
}
private SampleBlock radioButtonSample() {
var box = new VBox(V_GAP);
var group = new ToggleGroup();
var opt1 = new RadioButton("Option 1");
opt1.setToggleGroup(group);
var opt2 = new RadioButton("Option 1");
opt2.setToggleGroup(group);
opt2.setSelected(true);
var opt3 = new RadioButton("Option 3");
opt3.setToggleGroup(group);
box.getChildren().setAll(opt1, opt2, opt3);
return new SampleBlock("Radio Buttons", box);
}
private SampleBlock toggleSwitchSample() {
var box = new VBox(V_GAP);
var switch1 = new ToggleSwitch();
var switch2 = new ToggleSwitch();
switch2.setSelected(true);
box.getChildren().setAll(switch1, switch2);
return new SampleBlock("Switches", box);
}
private SampleBlock comboBoxSample() {
var box = new HBox(H_GAP);
var comboBox = new ComboBox<String>();
comboBox.getItems().setAll("Option 1", "Option 2", "Option 3");
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());
var colorPicker = new ColorPicker();
colorPicker.setPrefWidth(COMBO_BOX_WIDTH);
colorPicker.setValue(Color.ORANGE);
box.getChildren().setAll(comboBox, choiceBox, datePicker, colorPicker);
return new SampleBlock("Combo Boxes", box);
}
private SampleBlock sliderSample() {
var box = new HBox(H_GAP);
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);
box.getChildren().setAll(slider, tickSlider);
return new SampleBlock("Sliders", box);
}
private SampleBlock textFieldSample() {
var box = new HBox(H_GAP);
var textField = new TextField("Text");
textField.setPrefWidth(BUTTON_WIDTH);
var passwordField = new PasswordField();
passwordField.setText(FAKER.internet().password());
passwordField.setPrefWidth(BUTTON_WIDTH);
box.getChildren().setAll(textField, passwordField);
return new SampleBlock("Text Fields", box);
}
private SampleBlock spinnerSample() {
var box = new HBox(H_GAP);
var spinner1 = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(spinner1);
spinner1.setPrefWidth(BUTTON_WIDTH);
var spinner2 = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(spinner2);
spinner2.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
spinner2.setPrefWidth(BUTTON_WIDTH);
box.getChildren().setAll(spinner1, spinner2);
return new SampleBlock("Spinners", box);
}
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 toggleButton(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;
}
@SuppressWarnings("SameParameterValue")
private MenuItem[] menuItems(int count) {
return IntStream.range(0, count)
.mapToObj(i -> new MenuItem(FAKER.babylon5().character()))
.toArray(MenuItem[]::new);
}
}

@ -0,0 +1,113 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.theme.Styles;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Pagination;
import javafx.scene.control.Separator;
import javafx.scene.control.Spinner;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
public class PaginationPage extends AbstractPage {
public static final String NAME = "Pagination";
private static final int PREF_CONTROL_WIDTH = 120;
@Override
public String getName() { return NAME; }
public PaginationPage() {
super();
createView();
}
private void createView() {
var playground = new VBox(10);
playground.setMinHeight(100);
playground.setAlignment(Pos.CENTER);
var pagination = new Pagination();
pagination.setCurrentPageIndex(1);
pagination.setPageFactory(index -> {
var label = new Label("Page #" + (index + 1));
label.setStyle("-fx-font-size: 3em;");
var page = new BorderPane();
page.setCenter(label);
return page;
});
// == CONTROLS ==
var pageCountSpinner = new Spinner<Integer>(0, 50, 25);
pageCountSpinner.setPrefWidth(PREF_CONTROL_WIDTH);
pagination.pageCountProperty().bind(pageCountSpinner.valueProperty());
var visibleCountSpinner = new Spinner<Integer>(3, 10, 5);
visibleCountSpinner.setPrefWidth(PREF_CONTROL_WIDTH);
pagination.maxPageIndicatorCountProperty().bind(visibleCountSpinner.valueProperty());
var bulletToggle = new ToggleSwitch();
bulletToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(pagination, Pagination.STYLE_CLASS_BULLET)
);
var showArrowsToggle = new ToggleSwitch();
showArrowsToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { pagination.setStyle(String.format("-fx-arrows-visible: %s;", val)); }
});
showArrowsToggle.setSelected(true);
var showPageInfoToggle = new ToggleSwitch();
showPageInfoToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { pagination.setStyle(String.format("-fx-page-information-visible: %s;", val)); }
});
showPageInfoToggle.setSelected(true);
var disableToggle = new ToggleSwitch();
pagination.disableProperty().bind(disableToggle.selectedProperty());
var controls = new GridPane();
controls.setHgap(20);
controls.setVgap(10);
controls.add(new Label("Page count:"), 0, 0);
controls.add(pageCountSpinner, 1, 0);
controls.add(new Label("Visible count:"), 0, 1);
controls.add(visibleCountSpinner, 1, 1);
controls.add(new Label("Bullet style"), 3, 0);
controls.add(bulletToggle, 4, 0);
controls.add(new Label("Show arrows"), 3, 1);
controls.add(showArrowsToggle, 4, 1);
controls.add(new Label("Show info"), 5, 0);
controls.add(showPageInfoToggle, 6, 0);
controls.add(new Label("Disable"), 5, 1);
controls.add(disableToggle, 6, 1);
// ~
var separator = new Separator();
separator.getStyleClass().addAll(Styles.LARGE);
playground.getChildren().setAll(
pagination,
separator,
new HBox(new Spacer(), controls, new Spacer())
);
userContent.getChildren().setAll(playground);
}
}

@ -0,0 +1,108 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
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.SampleBlock;
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;
import java.time.LocalDate;
public class PopoverPage extends AbstractPage {
public static final String NAME = "Popover";
@Override
public String getName() { return NAME; }
public PopoverPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(basicSamples(), positionSamples().getRoot());
}
private HBox basicSamples() {
var textPopover = new Popover(textFlow(30));
textPopover.setTitle("Lorem Ipsum");
textPopover.setHeaderAlwaysVisible(true);
textPopover.setDetachable(true);
var textLink = hyperlink("Click me");
textLink.setOnAction(e -> textPopover.show(textLink));
var textBlock = new SampleBlock("Basic", textLink);
var datePicker = new InlineDatePicker();
datePicker.setValue(LocalDate.now());
var datePopover = new Popover(datePicker);
textPopover.setHeaderAlwaysVisible(false);
datePopover.setDetachable(true);
var dateLink = hyperlink("Click me");
dateLink.setOnAction(e -> datePopover.show(dateLink));
var dateBlock = new SampleBlock("Date picker", dateLink);
var box = new HBox(20,
textBlock.getRoot(),
dateBlock.getRoot()
);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
private SampleBlock positionSamples() {
var grid = new GridPane();
grid.setHgap(20);
grid.setVgap(20);
grid.add(arrowPositionBlock(ArrowLocation.TOP_LEFT), 0, 0);
grid.add(arrowPositionBlock(ArrowLocation.TOP_CENTER), 0, 1);
grid.add(arrowPositionBlock(ArrowLocation.TOP_RIGHT), 0, 2);
grid.add(arrowPositionBlock(ArrowLocation.RIGHT_TOP), 1, 0);
grid.add(arrowPositionBlock(ArrowLocation.RIGHT_CENTER), 1, 1);
grid.add(arrowPositionBlock(ArrowLocation.RIGHT_BOTTOM), 1, 2);
grid.add(arrowPositionBlock(ArrowLocation.BOTTOM_LEFT), 2, 0);
grid.add(arrowPositionBlock(ArrowLocation.BOTTOM_CENTER), 2, 1);
grid.add(arrowPositionBlock(ArrowLocation.BOTTOM_RIGHT), 2, 2);
grid.add(arrowPositionBlock(ArrowLocation.LEFT_TOP), 3, 0);
grid.add(arrowPositionBlock(ArrowLocation.LEFT_CENTER), 3, 1);
grid.add(arrowPositionBlock(ArrowLocation.LEFT_BOTTOM), 3, 2);
return new SampleBlock("Position", grid);
}
private Hyperlink hyperlink(String text) {
Hyperlink hyperlink = new Hyperlink(text);
hyperlink.setMinWidth(50);
hyperlink.setMinHeight(50);
hyperlink.setAlignment(Pos.CENTER_LEFT);
return hyperlink;
}
private TextFlow textFlow(int wordCount) {
var textFlow = new TextFlow(new Text(FAKER.lorem().sentence(wordCount)));
textFlow.setPrefWidth(300);
return textFlow;
}
private Hyperlink arrowPositionBlock(ArrowLocation arrowLocation) {
var link = hyperlink(String.valueOf(arrowLocation));
var popover = new Popover(textFlow(50));
popover.setHeaderAlwaysVisible(false);
popover.setArrowLocation(arrowLocation);
link.setOnAction(e -> popover.show(link));
return link;
}
}

@ -0,0 +1,90 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
// Indeterminate (animated) progress bar and also progress indicator are very expensive.
// It consumes single CPU core and a lot of memory.
// #javafx-bug
public class ProgressPage extends AbstractPage {
public static final String NAME = "Progress";
@Override
public String getName() { return NAME; }
public ProgressPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(
basicBarSamples().getRoot(),
basicIndicatorSamples().getRoot(),
barSizeSamples().getRoot()
);
}
private SampleBlock basicBarSamples() {
var flowPane = new FlowPane(20, 20);
flowPane.setAlignment(Pos.CENTER_LEFT);
flowPane.getChildren().addAll(
progressBar(0, false),
progressBar(0.5, false),
progressBar(1, false),
progressBar(0.5, true)
);
return new SampleBlock("Progress Bar", flowPane);
}
private SampleBlock basicIndicatorSamples() {
var flowPane = new FlowPane(20, 20);
flowPane.getChildren().addAll(
progressIndicator(0, false),
progressIndicator(0.5, false),
progressIndicator(1, false),
progressIndicator(0.5, true)
);
flowPane.setAlignment(Pos.TOP_LEFT);
return new SampleBlock("Progress Indicator", flowPane);
}
private SampleBlock barSizeSamples() {
var container = new VBox(
10,
new HBox(20, progressBar(0.5, false, Styles.SMALL), new Text("small")),
new HBox(20, progressBar(0.5, false, Styles.MEDIUM), new Text("medium")),
new HBox(20, progressBar(0.5, false, Styles.LARGE), new Text("large"))
);
container.getChildren().forEach(c -> ((HBox) c).setAlignment(Pos.CENTER_LEFT));
return new SampleBlock("Size", container);
}
private ProgressIndicator progressBar(double progress, boolean disabled, String... styleClasses) {
var bar = new ProgressBar(progress);
bar.getStyleClass().addAll(styleClasses);
bar.setDisable(disabled);
return bar;
}
private ProgressIndicator progressIndicator(double progress, boolean disabled) {
var indicator = new ProgressIndicator(progress);
indicator.setMinSize(50, 50);
indicator.setMaxSize(50, 50);
indicator.setDisable(disabled);
return indicator;
}
}

@ -0,0 +1,64 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
public class RadioButtonPage extends AbstractPage {
public static final String NAME = "RadioButton";
@Override
public String getName() { return NAME; }
public RadioButtonPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(
basicSamples(),
groupSamples().getRoot()
);
}
private HBox basicSamples() {
var basicRadio = new RadioButton("_Check Me");
basicRadio.setMnemonicParsing(true);
basicRadio.setOnAction(PRINT_SOURCE);
var basicBlock = new SampleBlock("Basic", basicRadio);
var disabledRadio = new RadioButton("Check Me");
disabledRadio.setDisable(true);
var disabledBlock = new SampleBlock("Disabled", disabledRadio);
var root = new HBox(20);
root.getChildren().addAll(
basicBlock.getRoot(),
disabledBlock.getRoot()
);
return root;
}
private SampleBlock groupSamples() {
var group = new ToggleGroup();
var musicRadio = new RadioButton("Music");
musicRadio.setToggleGroup(group);
musicRadio.setSelected(true);
var imagesRadio = new RadioButton("Images");
imagesRadio.setToggleGroup(group);
var videosRadio = new RadioButton("Videos");
videosRadio.setToggleGroup(group);
return new SampleBlock("Toggle Group", new VBox(5, musicRadio, imagesRadio, videosRadio));
}
}

@ -0,0 +1,85 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class ScrollPanePage extends AbstractPage {
public static final String NAME = "ScrollPane";
@Override
public String getName() { return NAME; }
public ScrollPanePage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(new FlowPane(20, 20,
hScrollBlock().getRoot(),
vScrollBlock().getRoot(),
gridScrollBlock().getRoot(),
disabledBlock().getRoot()
));
}
private SampleBlock hScrollBlock() {
var scrollPane = new ScrollPane();
scrollPane.setMaxHeight(100);
scrollPane.setMaxWidth(300);
scrollPane.setContent(new HBox(2,
new Rectangle(200, 100, Color.GREEN),
new Rectangle(200, 100, Color.RED)
));
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
return new SampleBlock("Horizontal scrolling", scrollPane);
}
private SampleBlock vScrollBlock() {
var scrollPane = new ScrollPane();
scrollPane.setMaxHeight(100);
scrollPane.setMaxWidth(300);
scrollPane.setContent(new VBox(2,
new Rectangle(300, 75, Color.GREEN),
new Rectangle(300, 75, Color.RED)
));
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
return new SampleBlock("Vertical scrolling", scrollPane);
}
private SampleBlock gridScrollBlock() {
var grid = new GridPane();
grid.add(new Rectangle(200, 75, Color.GREEN), 0, 0);
grid.add(new Rectangle(200, 75, Color.RED), 1, 0);
grid.add(new Rectangle(200, 75, Color.RED), 0, 1);
grid.add(new Rectangle(200, 75, Color.GREEN), 1, 1);
grid.setHgap(2);
grid.setVgap(2);
var gridScroll = new ScrollPane();
gridScroll.setMaxHeight(100);
gridScroll.setMaxWidth(300);
gridScroll.setContent(grid);
return new SampleBlock("Horizontal & vertical scrolling", gridScroll);
}
private SampleBlock disabledBlock() {
var block = gridScrollBlock();
block.setText("Disabled");
block.getContent().setDisable(true);
return block;
}
}

@ -0,0 +1,104 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Orientation;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.*;
import static javafx.geometry.Orientation.HORIZONTAL;
import static javafx.geometry.Orientation.VERTICAL;
import static javafx.geometry.Pos.CENTER;
public final class SeparatorPage extends AbstractPage {
public static final String NAME = "Separator";
private static final int BRICK_SIZE = 100;
@Override
public String getName() { return NAME; }
public SeparatorPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(
orientationSamples(),
sizeSamples()
);
}
private FlowPane orientationSamples() {
var hBox = new HBox(brick("Left", VERTICAL), new Separator(VERTICAL), brick("Right", VERTICAL));
hBox.setAlignment(CENTER);
var hBlock = new SampleBlock("Vertical", hBox);
var vBox = new VBox(brick("Top", HORIZONTAL), new Separator(HORIZONTAL), brick("Bottom", HORIZONTAL));
vBox.setAlignment(CENTER);
var vBlock = new SampleBlock("Horizontal", vBox);
var root = new FlowPane(20, 20);
root.getChildren().setAll(
hBlock.getRoot(),
vBlock.getRoot()
);
return root;
}
private FlowPane sizeSamples() {
var smallSep = new Separator(VERTICAL);
smallSep.getStyleClass().add(Styles.SMALL);
var smallBox = new HBox(brick("Left", VERTICAL), smallSep, brick("Right", VERTICAL));
smallBox.setAlignment(CENTER);
var smallBlock = new SampleBlock("Small", smallBox);
var mediumSep = new Separator(VERTICAL);
mediumSep.getStyleClass().add(Styles.MEDIUM);
var mediumBox = new HBox(brick("Left", VERTICAL), mediumSep, brick("Right", VERTICAL));
mediumBox.setAlignment(CENTER);
var mediumBlock = new SampleBlock("Medium", mediumBox);
var largeSep = new Separator(VERTICAL);
largeSep.getStyleClass().add(Styles.LARGE);
var largeBox = new HBox(brick("Left", VERTICAL), largeSep, brick("Right", VERTICAL));
largeBox.setAlignment(CENTER);
var largeBlock = new SampleBlock("Large", largeBox);
var root = new FlowPane(20, 20);
root.getChildren().setAll(
smallBlock.getRoot(),
mediumBlock.getRoot(),
largeBlock.getRoot()
);
return root;
}
private Pane brick(String text, Orientation orientation) {
var root = new StackPane();
root.getChildren().setAll(new Label(text));
root.getStyleClass().add("bordered");
if (orientation == HORIZONTAL) {
root.setMinHeight(BRICK_SIZE);
root.setPrefHeight(BRICK_SIZE);
root.setMaxHeight(BRICK_SIZE);
root.setMinWidth(BRICK_SIZE);
}
if (orientation == VERTICAL) {
root.setMinWidth(BRICK_SIZE);
root.setPrefWidth(BRICK_SIZE);
root.setMaxWidth(BRICK_SIZE);
root.setMinHeight(BRICK_SIZE);
}
return root;
}
}

@ -0,0 +1,72 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.Slider;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import static javafx.geometry.Orientation.HORIZONTAL;
import static javafx.geometry.Orientation.VERTICAL;
public class SliderPage extends AbstractPage {
public static final String NAME = "Slider";
@Override
public String getName() { return NAME; }
public SliderPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(row1(), row2());
}
private Pane row1() {
var slider = new Slider(1, 5, 3);
slider.setOrientation(HORIZONTAL);
var tickSlider = tickSlider();
tickSlider.setMinWidth(200);
tickSlider.setMaxWidth(200);
var hBlock = new SampleBlock("Horizontal", new HBox(20, slider, tickSlider));
return new HBox(20, hBlock.getRoot());
}
private Pane row2() {
var slider = new Slider(1, 5, 3);
slider.setOrientation(VERTICAL);
var tickSlider = tickSlider();
tickSlider.setOrientation(VERTICAL);
tickSlider.setMinHeight(200);
tickSlider.setMaxHeight(200);
var vBlock = new SampleBlock("Vertical", new HBox(20, slider, tickSlider));
var disabledSlider = tickSlider();
disabledSlider.setDisable(true);
var disabledBlock = new SampleBlock("Disabled", new HBox(20, disabledSlider));
return new HBox(40, vBlock.getRoot(), disabledBlock.getRoot());
}
private Slider tickSlider() {
var slider = new Slider(0, 5, 3);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(1);
slider.setBlockIncrement(1);
slider.setMinorTickCount(5);
slider.setSnapToTicks(true);
return slider;
}
}

@ -0,0 +1,98 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.util.IntegerStringConverter;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.Spinner;
import javafx.scene.layout.FlowPane;
public final class SpinnerPage extends AbstractPage {
public static final String NAME = "Spinner";
private static final int PREF_WIDTH = 120;
@Override
public String getName() { return NAME; }
public SpinnerPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().addAll(
basicSamples(),
arrowPositionSamples()
);
}
private FlowPane basicSamples() {
var editableSpin = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(editableSpin);
editableSpin.setPrefWidth(PREF_WIDTH);
editableSpin.setEditable(true);
var editableBlock = new SampleBlock("Editable", editableSpin);
var disabledSpin = new Spinner<Integer>(1, 10, 1);
disabledSpin.setPrefWidth(PREF_WIDTH);
disabledSpin.setDisable(true);
var disabledBlock = new SampleBlock("Disabled", disabledSpin);
var root = new FlowPane(20, 20);
root.getChildren().addAll(
editableBlock.getRoot(),
disabledBlock.getRoot()
);
return root;
}
private FlowPane arrowPositionSamples() {
var leftVSpin = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(leftVSpin);
leftVSpin.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL);
leftVSpin.setPrefWidth(PREF_WIDTH);
leftVSpin.setEditable(true);
var leftVBlock = new SampleBlock("Arrows on left & vertical", leftVSpin);
var leftHSpin = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(leftHSpin);
leftHSpin.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL);
leftHSpin.setPrefWidth(PREF_WIDTH);
leftHSpin.setEditable(true);
var leftHBlock = new SampleBlock("Arrows on left & horizontal", leftHSpin);
var rightHSpin = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(rightHSpin);
rightHSpin.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL);
rightHSpin.setPrefWidth(PREF_WIDTH);
rightHSpin.setEditable(true);
var rightHBlock = new SampleBlock("Arrows on right & horizontal", rightHSpin);
var splitHSpin = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(splitHSpin);
splitHSpin.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
splitHSpin.setPrefWidth(PREF_WIDTH);
splitHSpin.setEditable(true);
var splitHBlock = new SampleBlock("Split arrows & horizontal", splitHSpin);
var splitVSpin = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(splitVSpin);
splitVSpin.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL);
splitVSpin.setEditable(true);
splitVSpin.setPrefWidth(40);
var splitVBlock = new SampleBlock("Split arrows & vertical", splitVSpin);
var root = new FlowPane(20, 20);
root.getChildren().addAll(
leftVBlock.getRoot(),
leftHBlock.getRoot(),
rightHBlock.getRoot(),
splitHBlock.getRoot(),
splitVBlock.getRoot()
);
return root;
}
}

@ -0,0 +1,103 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
public class SplitPanePage extends AbstractPage {
public static final String NAME = "SplitPane";
@Override
public String getName() { return NAME; }
public SplitPanePage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(new FlowPane(20, 20,
hSplitBlock().getRoot(),
vSplitBlock().getRoot(),
disabledSplitBlock().getRoot(),
gridSplitBlock().getRoot()
));
}
private SampleBlock hSplitBlock() {
var splitPane = new SplitPane();
splitPane.setOrientation(Orientation.HORIZONTAL);
splitPane.setDividerPositions(0.5);
splitPane.getItems().setAll(hBrick("Left"), hBrick("Right"));
splitPane.setMinSize(200, 100);
splitPane.setMaxSize(200, 100);
return new SampleBlock("Horizontal", splitPane);
}
private SampleBlock vSplitBlock() {
var splitPane = new SplitPane();
splitPane.setOrientation(Orientation.VERTICAL);
splitPane.setDividerPositions(0.5);
splitPane.getItems().setAll(vBrick("Top"), hBrick("Bottom"));
splitPane.setMinSize(100, 200);
splitPane.setMaxSize(100, 200);
return new SampleBlock("Vertical", splitPane);
}
private SampleBlock gridSplitBlock() {
var topSplitPane = new SplitPane();
topSplitPane.setOrientation(Orientation.HORIZONTAL);
topSplitPane.setDividerPositions(0.5);
topSplitPane.getItems().setAll(vBrick("Quarter 4"), hBrick("Quarter 1"));
VBox.setVgrow(topSplitPane, Priority.ALWAYS);
var bottomSplitPane = new SplitPane();
bottomSplitPane.setOrientation(Orientation.HORIZONTAL);
bottomSplitPane.setDividerPositions(0.5);
bottomSplitPane.getItems().setAll(vBrick("Quarter 3"), hBrick("Quarter 2"));
VBox.setVgrow(bottomSplitPane, Priority.ALWAYS);
var doubleSplitPane = new SplitPane();
doubleSplitPane.setOrientation(Orientation.VERTICAL);
doubleSplitPane.setDividerPositions(0.5);
doubleSplitPane.getItems().setAll(
new VBox(topSplitPane) {{ setAlignment(Pos.CENTER); }},
new VBox(bottomSplitPane) {{ setAlignment(Pos.CENTER); }}
);
doubleSplitPane.setMinSize(400, 200);
doubleSplitPane.setMaxSize(400, 200);
return new SampleBlock("Nested", doubleSplitPane);
}
private SampleBlock disabledSplitBlock() {
var block = hSplitBlock();
block.setText("Disabled");
block.getContent().setDisable(true);
return block;
}
private HBox hBrick(String text) {
var brick = new HBox(new Text(text));
brick.setAlignment(Pos.CENTER);
return brick;
}
private VBox vBrick(String text) {
var brick = new VBox(new Text(text));
brick.setAlignment(Pos.CENTER);
return brick;
}
}

@ -0,0 +1,244 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.theme.Styles;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.base.theme.Styles.ACCENT;
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
import static javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE;
public class TabPanePage extends AbstractPage {
public static final String NAME = "TabPane";
private static final double TAB_MIN_HEIGHT = 60;
@Override
public String getName() { return NAME; }
private Side tabSide = Side.TOP;
private boolean fullWidth = false;
public TabPanePage() {
super();
createView();
}
private void createView() {
var tabs = tabPane();
var tabsLayer = new BorderPane();
tabsLayer.setTop(tabs);
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> updateTabsWidth(tabsLayer, tabs, fullWidth));
var controller = createController(tabsLayer, tabs);
controller.setPrefSize(500, 300);
var controllerLayer = new BorderPane();
controllerLayer.setCenter(controller);
controllerLayer.setMaxSize(500, 300);
var root = new StackPane();
root.getStyleClass().add(Styles.BORDERED);
root.getChildren().addAll(tabsLayer, controllerLayer);
VBox.setVgrow(root, Priority.ALWAYS);
userContent.getChildren().setAll(root);
}
private TitledPane createController(BorderPane borderPane, TabPane tabs) {
// == BUTTONS ==
var toTopBtn = new Button("", new FontIcon(Feather.ARROW_UP));
toTopBtn.getStyleClass().addAll(BUTTON_ICON);
toTopBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.TOP));
var toRightBtn = new Button("", new FontIcon(Feather.ARROW_RIGHT));
toRightBtn.getStyleClass().addAll(BUTTON_ICON);
toRightBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.RIGHT));
var toBottomBtn = new Button("", new FontIcon(Feather.ARROW_DOWN));
toBottomBtn.getStyleClass().addAll(BUTTON_ICON);
toBottomBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.BOTTOM));
var toLeftBtn = new Button("", new FontIcon(Feather.ARROW_LEFT));
toLeftBtn.getStyleClass().addAll(BUTTON_ICON);
toLeftBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.LEFT));
var appendBtn = new Button("", new FontIcon(Feather.PLUS));
appendBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
appendBtn.setOnAction(e -> tabs.getTabs().add(randomTab()));
var buttonsPane = new BorderPane();
buttonsPane.setMinSize(120, 120);
buttonsPane.setMaxSize(120, 120);
buttonsPane.setTop(toTopBtn);
BorderPane.setAlignment(toTopBtn, Pos.CENTER);
buttonsPane.setRight(toRightBtn);
BorderPane.setAlignment(toRightBtn, Pos.CENTER);
buttonsPane.setBottom(toBottomBtn);
BorderPane.setAlignment(toBottomBtn, Pos.CENTER);
buttonsPane.setLeft(toLeftBtn);
BorderPane.setAlignment(toLeftBtn, Pos.CENTER);
buttonsPane.setCenter(appendBtn);
// == TOGGLES ==
var closeableToggle = new ToggleSwitch();
closeableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val) {
tabs.setTabClosingPolicy(ALL_TABS);
} else {
tabs.setTabClosingPolicy(UNAVAILABLE);
}
});
var floatingToggle = new ToggleSwitch();
floatingToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); }
});
var fullWidthToggle = new ToggleSwitch();
fullWidthToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) {
updateTabsWidth(borderPane, tabs, val);
fullWidth = val;
}
});
var disableToggle = new ToggleSwitch();
disableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { tabs.setDisable(val); }
});
var togglesGrid = new GridPane();
togglesGrid.setHgap(10);
togglesGrid.setVgap(10);
togglesGrid.add(gridLabel("Closeable"), 0, 0);
togglesGrid.add(closeableToggle, 1, 0);
togglesGrid.add(gridLabel("Floating"), 0, 1);
togglesGrid.add(floatingToggle, 1, 1);
togglesGrid.add(gridLabel("Full width"), 0, 2);
togglesGrid.add(fullWidthToggle, 1, 2);
togglesGrid.add(gridLabel("Disable"), 0, 3);
togglesGrid.add(disableToggle, 1, 3);
// == LAYOUT ==
var controls = new HBox(40,
new Spacer(),
buttonsPane,
togglesGrid,
new Spacer()
);
controls.setAlignment(Pos.CENTER);
var content = new VBox(20);
content.getChildren().setAll(controls);
content.setAlignment(Pos.CENTER);
var root = new TitledPane("Controller", content);
root.setCollapsible(false);
return root;
}
private void updateTabsWidth(BorderPane borderPane, TabPane tabs, boolean val) {
tabs.tabMinWidthProperty().unbind();
// reset tab width
if (!val) {
tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE);
return;
}
// There are two issues with full-width tabs.
// - minWidth is applied to the tab itself but to internal .tab-container,
// thus we have to subtract tab paddings that are normally set via CSS.
// - .control-buttons-tab appears automatically and can't be disabled via
// TabPane property.
// Overall this feature should be supported by the TabPane internally, otherwise
// it's hard to make it work properly.
if (tabs.getSide() == Side.TOP || tabs.getSide() == Side.BOTTOM) {
tabs.tabMinWidthProperty().bind(borderPane.widthProperty()
.subtract(18) // .control-buttons-tab width
.divide(tabs.getTabs().size())
.subtract(28) // .tab paddings
);
}
if (tabs.getSide() == Side.LEFT || tabs.getSide() == Side.RIGHT) {
tabs.tabMinWidthProperty().bind(borderPane.heightProperty()
.subtract(18) // same as above
.divide(tabs.getTabs().size())
.subtract(28)
);
}
}
private TabPane tabPane() {
var tabs = new TabPane();
tabs.setTabClosingPolicy(UNAVAILABLE);
tabs.setMinHeight(TAB_MIN_HEIGHT);
// NOTE: Individually disabled tab is still closeable even while it looks
// like disabled. To prevent it from closing one can use "black hole"
// event handler. #javafx-bug
tabs.getTabs().addAll(
randomTab(),
randomTab(),
randomTab()
);
return tabs;
}
private void rotateTabs(BorderPane borderPane, TabPane tabs, Side side) {
if (tabSide == side) { return; }
borderPane.getChildren().removeAll(tabs);
tabSide = side;
Platform.runLater(() -> {
tabs.setSide(side);
switch (side) {
case TOP -> borderPane.setTop(tabs);
case RIGHT -> borderPane.setRight(tabs);
case BOTTOM -> borderPane.setBottom(tabs);
case LEFT -> borderPane.setLeft(tabs);
}
updateTabsWidth(borderPane, tabs, fullWidth);
});
}
private Tab randomTab() {
var tab = new Tab(FAKER.cat().name());
tab.setGraphic(new FontIcon(randomIcon()));
return tab;
}
private Label gridLabel(String text) {
var label = new Label(text);
label.setAlignment(Pos.CENTER_RIGHT);
label.setMaxWidth(Double.MAX_VALUE);
return label;
}
}

@ -0,0 +1,247 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.sampler.fake.domain.Product;
import atlantafx.base.controls.CaptionMenuItem;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
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.*;
import javafx.scene.control.cell.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIconTableCell;
import java.util.List;
import java.util.stream.IntStream;
import static atlantafx.base.theme.Styles.*;
import static javafx.collections.FXCollections.observableArrayList;
import static javafx.geometry.Orientation.HORIZONTAL;
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();
createView();
}
private void createView() {
userContent.getChildren().setAll(
playground()
);
}
private VBox playground() {
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 disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { table.setDisable(val); }
});
var maxValue = 100;
var rowCountChoice = new ComboBox<>(observableArrayList(0, 5, 10, 25, maxValue));
rowCountChoice.setValue(maxValue);
var rowCountBox = new HBox(10, new Label("rows"), rowCountChoice);
rowCountBox.setAlignment(Pos.CENTER_LEFT);
var togglesBox = new HBox(20,
bordersToggle,
denseToggle,
stripesToggle,
disableToggle,
new Spacer(HORIZONTAL),
rowCountBox
);
togglesBox.setAlignment(Pos.CENTER_LEFT);
// ~
var filteredData = new FilteredList<>(observableArrayList(dataList));
filteredData.predicateProperty().bind(Bindings.createObjectBinding(
() -> product -> product.getId() <= rowCountChoice.getValue(),
rowCountChoice.valueProperty()
));
var sortedData = new SortedList<>(filteredData);
table = table();
table.setItems(sortedData);
sortedData.comparatorProperty().bind(table.comparatorProperty());
// ~
var topBox = new HBox(
new Label("Example:"),
new Spacer(),
settingsMenu(table)
);
topBox.setAlignment(Pos.CENTER_LEFT);
var playground = new VBox(10);
playground.setMinHeight(100);
playground.getChildren().setAll(
topBox,
table,
togglesBox
);
return playground;
}
@SuppressWarnings("unchecked")
private TableView<Product> table() {
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 table = new TableView<Product>();
table.getColumns().setAll(stateCol, indexCol, iconCol, brandCol, nameCol, priceCol, stockCol);
return table;
}
private MenuButton settingsMenu(TableView<Product> table) {
var resizePolicyCaption = new CaptionMenuItem("Resize Policy");
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);
}
});
var unconstrainedResizeItem = new RadioMenuItem("Unconstrained");
unconstrainedResizeItem.setToggleGroup(resizePolicyGroup);
unconstrainedResizeItem.setUserData(TableView.UNCONSTRAINED_RESIZE_POLICY);
unconstrainedResizeItem.setSelected(true);
var constrainedResizeItem = new RadioMenuItem("Constrained");
constrainedResizeItem.setToggleGroup(resizePolicyGroup);
constrainedResizeItem.setUserData(TableView.CONSTRAINED_RESIZE_POLICY);
// ~
var selectionModeCaption = new CaptionMenuItem("Selection Mode");
var selectionModeGroup = new ToggleGroup();
selectionModeGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof SelectionMode mode) {
table.getSelectionModel().setSelectionMode(mode);
}
});
var singleSelectionItem = new RadioMenuItem("Single");
singleSelectionItem.setToggleGroup(selectionModeGroup);
singleSelectionItem.setUserData(SelectionMode.SINGLE);
var multiSelectionItem = new RadioMenuItem("Multiple");
multiSelectionItem.setToggleGroup(selectionModeGroup);
multiSelectionItem.setUserData(SelectionMode.MULTIPLE);
multiSelectionItem.setSelected(true);
// ~
var editCellsItem = new CheckMenuItem("Editable");
table.editableProperty().bind(editCellsItem.selectedProperty());
editCellsItem.setSelected(true);
var cellSelectionItem = new CheckMenuItem("Enable cell selection");
table.getSelectionModel().cellSelectionEnabledProperty().bind(cellSelectionItem.selectedProperty());
cellSelectionItem.setSelected(false);
var menuButtonItem = new CheckMenuItem("Show menu button");
table.tableMenuButtonVisibleProperty().bind(menuButtonItem.selectedProperty());
menuButtonItem.setSelected(true);
return new MenuButton("Settings") {{
getItems().setAll(
resizePolicyCaption,
unconstrainedResizeItem,
constrainedResizeItem,
selectionModeCaption,
singleSelectionItem,
multiSelectionItem,
new SeparatorMenuItem(),
editCellsItem,
cellSelectionItem,
menuButtonItem
);
}};
}
}

@ -0,0 +1,86 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.TextArea;
import javafx.scene.layout.FlowPane;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static atlantafx.base.theme.Styles.STATE_DANGER;
import static atlantafx.base.theme.Styles.STATE_SUCCESS;
public class TextAreaPage extends AbstractPage {
public static final String NAME = "TextArea";
private static final double CONTROL_WIDTH = 200;
private static final double CONTROL_HEIGHT = 120;
@Override
public String getName() { return NAME; }
public TextAreaPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(samples());
}
private FlowPane samples() {
var basicArea = textArea("Text");
basicArea.setWrapText(true);
var basicBlock = new SampleBlock("Basic", basicArea);
var promptArea = textArea(null);
promptArea.setPromptText("Prompt text");
var promptBlock = new SampleBlock("Prompt", promptArea);
var scrollArea = textArea(
Stream.generate(() -> FAKER.lorem().paragraph()).limit(10).collect(Collectors.joining("\n"))
);
scrollArea.setWrapText(false);
var scrollBlock = new SampleBlock("Scrolling", scrollArea);
var readonlyArea = textArea("Text");
readonlyArea.setEditable(false);
var readonlyBlock = new SampleBlock("Readonly", readonlyArea);
var disabledArea = textArea("Text");
disabledArea.setDisable(true);
var disabledBlock = new SampleBlock("Disabled", disabledArea);
var successArea = textArea("Text");
successArea.pseudoClassStateChanged(STATE_SUCCESS, true);
var successBlock = new SampleBlock("Success", successArea);
var dangerArea = textArea("Text");
dangerArea.pseudoClassStateChanged(STATE_DANGER, true);
var dangerBlock = new SampleBlock("Danger", dangerArea);
var flowPane = new FlowPane(20, 20);
flowPane.getChildren().setAll(
basicBlock.getRoot(),
promptBlock.getRoot(),
scrollBlock.getRoot(),
readonlyBlock.getRoot(),
disabledBlock.getRoot(),
successBlock.getRoot(),
dangerBlock.getRoot()
);
return flowPane;
}
private TextArea textArea(String text) {
var textArea = new TextArea(text);
textArea.setMinWidth(CONTROL_WIDTH);
textArea.setMinHeight(CONTROL_HEIGHT);
textArea.setMaxWidth(CONTROL_WIDTH);
textArea.setMaxHeight(CONTROL_HEIGHT);
return textArea;
}
}

@ -0,0 +1,70 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import static atlantafx.base.theme.Styles.STATE_DANGER;
import static atlantafx.base.theme.Styles.STATE_SUCCESS;
public class TextFieldPage extends AbstractPage {
public static final String NAME = "TextField";
@Override
public String getName() { return NAME; }
public TextFieldPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(samples());
}
private FlowPane samples() {
var basicField = new TextField("Text");
var basicBlock = new SampleBlock("Basic", basicField);
var passwordField = new PasswordField();
passwordField.setText("qwerty");
var passwordBlock = new SampleBlock("Password", passwordField);
var promptField = new TextField();
promptField.setPromptText("Prompt text");
var promptBlock = new SampleBlock("Prompt", promptField);
var readonlyField = new TextField("Text");
readonlyField.setEditable(false);
var readonlyBlock = new SampleBlock("Readonly", readonlyField);
var disabledField = new TextField("Text");
disabledField.setDisable(true);
var disabledBlock = new SampleBlock("Disabled", disabledField);
var successField = new TextField("Text");
successField.pseudoClassStateChanged(STATE_SUCCESS, true);
var successBlock = new SampleBlock("Success", successField);
var dangerField = new TextField("Text");
dangerField.pseudoClassStateChanged(STATE_DANGER, true);
var dangerBlock = new SampleBlock("Danger", dangerField);
var flowPane = new FlowPane(20, 20);
flowPane.getChildren().setAll(
basicBlock.getRoot(),
passwordBlock.getRoot(),
promptBlock.getRoot(),
readonlyBlock.getRoot(),
disabledBlock.getRoot(),
successBlock.getRoot(),
dangerBlock.getRoot()
);
return flowPane;
}
}

@ -0,0 +1,120 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import static atlantafx.base.theme.Styles.ELEVATED_2;
import static atlantafx.base.theme.Styles.INTERACTIVE;
public class TitledPanePage extends AbstractPage {
public static final String NAME = "TitledPane";
private static final String ELEVATED_PREFIX = "elevated-";
@Override
public String getName() { return NAME; }
public TitledPanePage() {
super();
createView();
}
private void createView() {
var samples = new HBox(20, interactivePane(), disabledCard(), untitledCard());
samples.getChildren().forEach(c -> ((TitledPane) c).setPrefSize(500, 120));
userContent.getChildren().setAll(new VBox(20, playground(), samples));
}
private TitledPane playground() {
var playground = new TitledPane();
playground.setText("_Playground");
playground.setMnemonicParsing(true);
playground.getStyleClass().add(ELEVATED_2);
var textFlow = new TextFlow(new Text(FAKER.lorem().paragraph(10)));
textFlow.setMinHeight(Region.USE_PREF_SIZE);
textFlow.setMaxHeight(Region.USE_PREF_SIZE);
textFlow.setLineSpacing(5);
var elevationSlider = new Slider(0, 4, 2);
elevationSlider.setShowTickLabels(true);
elevationSlider.setShowTickMarks(true);
elevationSlider.setMajorTickUnit(1);
elevationSlider.setBlockIncrement(1);
elevationSlider.setMinorTickCount(0);
elevationSlider.setSnapToTicks(true);
elevationSlider.setMinWidth(150);
elevationSlider.setMaxWidth(150);
elevationSlider.valueProperty().addListener((obs, old, val) -> {
playground.getStyleClass().removeAll(
playground.getStyleClass().stream().filter(c -> c.startsWith(ELEVATED_PREFIX)).toList()
);
if (val == null) { return; }
int level = val.intValue();
if (level > 0) { playground.getStyleClass().add(ELEVATED_PREFIX + level); }
});
// NOTE:
// Disabling 'collapsible' property leads to incorrect title layout,
// for some reason it still preserves arrow button gap. #javafx-bug
var collapseToggle = new ToggleSwitch("Collapsible");
collapseToggle.setSelected(true);
playground.collapsibleProperty().bind(collapseToggle.selectedProperty());
var animateToggle = new ToggleSwitch("Animated");
animateToggle.setSelected(true);
playground.animatedProperty().bind(animateToggle.selectedProperty());
var controls = new HBox(20);
controls.setMinHeight(80);
controls.setFillHeight(false);
controls.setAlignment(Pos.CENTER_LEFT);
controls.getChildren().setAll(
new Label("Elevation"),
elevationSlider,
new Spacer(),
collapseToggle,
animateToggle
);
var content = new VBox(20, textFlow, controls);
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 disabledCard() {
var titledPane = new TitledPane("Disabled", new CheckBox("This checkbox is disabled."));
titledPane.setCollapsible(false);
titledPane.setDisable(true);
return titledPane;
}
private TitledPane untitledCard() {
var titledPane = new TitledPane("This pane has no title.", new Text());
titledPane.setCollapsible(false);
return titledPane;
}
}

@ -0,0 +1,132 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import static atlantafx.base.theme.Styles.*;
import static atlantafx.sampler.util.Controls.toggleButton;
import static javafx.scene.layout.GridPane.REMAINING;
public class ToggleButtonPage extends AbstractPage {
public static final String NAME = "ToggleButton";
@Override
public String getName() { return NAME; }
public ToggleButtonPage() {
super();
createView();
}
private void createView() {
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(40);
grid.add(basicSample().getRoot(), 0, 0, REMAINING, 1);
grid.add(wizardSample().getRoot(), 0, 1);
grid.add(iconOnlySample().getRoot(), 1, 1);
grid.add(disabledSample().getRoot(), 0, 2);
userContent.getChildren().addAll(grid);
}
private SampleBlock basicSample() {
var threeButtonGroup = new ToggleGroup();
var leftPill = toggleButton("._left-pill", null, threeButtonGroup, true, LEFT_PILL);
leftPill.setMnemonicParsing(true);
leftPill.setOnAction(PRINT_SOURCE);
var centerPill = toggleButton("._center-pill", null, threeButtonGroup, false, CENTER_PILL);
centerPill.setMnemonicParsing(true);
centerPill.setOnAction(PRINT_SOURCE);
var rightPill = toggleButton("._right-pill", null, threeButtonGroup, false, RIGHT_PILL);
rightPill.setMnemonicParsing(true);
rightPill.setOnAction(PRINT_SOURCE);
var threeButtonBox = new HBox(leftPill, centerPill, rightPill);
var twoButtonGroup = new ToggleGroup();
var twoButtonBox = new HBox(
toggleButton(".left-pill", null, twoButtonGroup, true, LEFT_PILL),
toggleButton(".right-pill", null, twoButtonGroup, false, RIGHT_PILL)
);
var content = new HBox(10);
content.getChildren().setAll(threeButtonBox, twoButtonBox);
return new SampleBlock("Basic", content);
}
private SampleBlock wizardSample() {
var group = new ToggleGroup();
var prevBtn = new Button("\f");
prevBtn.getStyleClass().addAll(BUTTON_ICON, LEFT_PILL);
prevBtn.setGraphic(new FontIcon(Feather.CHEVRON_LEFT));
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");
nextBtn.getStyleClass().addAll(BUTTON_ICON, RIGHT_PILL);
nextBtn.setGraphic(new FontIcon(Feather.CHEVRON_RIGHT));
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) -> {
if (val == null) { old.setSelected(true); }
});
return new SampleBlock("Wizard", wizard);
}
private SampleBlock iconOnlySample() {
var icons = new HBox(
toggleButton("", Feather.BOLD, null, true, BUTTON_ICON, LEFT_PILL),
toggleButton("", Feather.ITALIC, null, false, BUTTON_ICON, CENTER_PILL),
toggleButton("", Feather.UNDERLINE, null, false, BUTTON_ICON, RIGHT_PILL)
);
return new SampleBlock("Icon only", icons);
}
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);
}
}

@ -0,0 +1,32 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.sampler.page.AbstractPage;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
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();
createView();
}
private void createView() {
var toggle = new ToggleSwitch();
toggle.selectedProperty().addListener((obs, old, val) -> toggle.setText(val ? "Disable" : "Enable"));
toggle.setSelected(true);
var box = new VBox(20, new Label("Nothing fancy here."), toggle);
box.setAlignment(Pos.CENTER);
userContent.getChildren().setAll(box);
}
}

@ -0,0 +1,285 @@
/* 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.sampler.fake.SampleMenuBar;
import atlantafx.sampler.page.AbstractPage;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.text.Font;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
import java.util.stream.IntStream;
import static atlantafx.base.theme.Styles.*;
import static atlantafx.sampler.util.Controls.*;
import static javafx.geometry.Orientation.HORIZONTAL;
import static javafx.geometry.Orientation.VERTICAL;
public class ToolBarPage extends AbstractPage {
public static final String NAME = "ToolBar";
@Override
public String getName() { return NAME; }
private Side toolbarPos = Side.TOP;
public ToolBarPage() {
super();
createView();
}
private void createView() {
var toolbar = new ToolBar(toolBarButtons(HORIZONTAL));
var toolbarLayer = new BorderPane();
toolbarLayer.setTop(new TopBar(toolbar));
var controller = createController(toolbarLayer, toolbar);
controller.setPrefSize(500, 300);
var controllerLayer = new BorderPane();
controllerLayer.setCenter(controller);
controllerLayer.setMaxSize(500, 300);
var root = new StackPane();
root.getStyleClass().add(Styles.BORDERED);
root.getChildren().addAll(toolbarLayer, controllerLayer);
VBox.setVgrow(root, Priority.ALWAYS);
userContent.getChildren().setAll(root);
}
private TitledPane createController(BorderPane borderPane, ToolBar toolbar) {
// == BUTTONS ==
var toTopBtn = new Button("", new FontIcon(Feather.ARROW_UP));
toTopBtn.getStyleClass().addAll(BUTTON_ICON);
toTopBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.TOP));
var toRightBtn = new Button("", new FontIcon(Feather.ARROW_RIGHT));
toRightBtn.getStyleClass().addAll(BUTTON_ICON);
toRightBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.RIGHT));
var toBottomBtn = new Button("", new FontIcon(Feather.ARROW_DOWN));
toBottomBtn.getStyleClass().addAll(BUTTON_ICON);
toBottomBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.BOTTOM));
var toLeftBtn = new Button("", new FontIcon(Feather.ARROW_LEFT));
toLeftBtn.getStyleClass().addAll(BUTTON_ICON);
toLeftBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.LEFT));
var appendBtn = new Button("", new FontIcon(Feather.PLUS));
appendBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
appendBtn.setOnAction(e -> {
if (toolbar.getOrientation() == HORIZONTAL) {
var textBtn = new Button(FAKER.animal().name(), new FontIcon(randomIcon()));
toolbar.getItems().add(textBtn);
} else {
var iconBtn = new Button("", new FontIcon(randomIcon()));
iconBtn.getStyleClass().addAll(BUTTON_ICON);
toolbar.getItems().add(iconBtn);
}
});
var buttonsPane = new BorderPane();
buttonsPane.setMinSize(120, 120);
buttonsPane.setMaxSize(120, 120);
buttonsPane.setTop(toTopBtn);
BorderPane.setAlignment(toTopBtn, Pos.CENTER);
buttonsPane.setRight(toRightBtn);
BorderPane.setAlignment(toRightBtn, Pos.CENTER);
buttonsPane.setBottom(toBottomBtn);
BorderPane.setAlignment(toBottomBtn, Pos.CENTER);
buttonsPane.setLeft(toLeftBtn);
BorderPane.setAlignment(toLeftBtn, Pos.CENTER);
buttonsPane.setCenter(appendBtn);
// == TOGGLES ==
var menuBarToggle = new ToggleSwitch();
menuBarToggle.selectedProperty().addListener((obs, old, value) -> {
TopBar topBar = (TopBar) borderPane.getTop();
if (value) {
topBar.showOrCreateMenuBar();
} else {
topBar.hideMenuBar();
}
});
var disableToggle = new ToggleSwitch();
disableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { toolbar.setDisable(val); }
});
var togglesGrid = new GridPane();
togglesGrid.setHgap(10);
togglesGrid.setVgap(10);
togglesGrid.add(gridLabel("Show menu bar"), 0, 0);
togglesGrid.add(menuBarToggle, 1, 0);
togglesGrid.add(gridLabel("Disable"), 0, 1);
togglesGrid.add(disableToggle, 1, 1);
// == LAYOUT ==
var controls = new HBox(40, new Spacer(), buttonsPane, togglesGrid, new Spacer());
controls.setAlignment(Pos.CENTER);
var content = new VBox(10);
content.getChildren().setAll(controls);
content.setAlignment(Pos.CENTER);
var root = new TitledPane("Controller", content);
root.setCollapsible(false);
return root;
}
private void rotateToolbar(BorderPane borderPane, ToolBar toolbar, Side pos) {
if (toolbarPos == pos) { return; }
var topBar = (TopBar) borderPane.getTop();
toolbarPos = pos;
boolean changed = borderPane.getChildren().removeAll(toolbar);
if (!changed) { topBar.removeToolBar(); }
// WARNING:
// Rotating existing buttons seems tempting, but it won't work.
// JavaFX doesn't recalculate their size correctly (even after
// reattaching controls to the scene), and you'll end up creating
// new objects anyway.
Platform.runLater(() -> {
switch (pos) {
case TOP -> {
toolbar.setOrientation(HORIZONTAL);
Styles.addStyleClass(toolbar, TOP, RIGHT, BOTTOM, LEFT);
toolbar.getItems().setAll(toolBarButtons(HORIZONTAL));
topBar.setToolBar(toolbar);
}
case RIGHT -> {
toolbar.setOrientation(VERTICAL);
Styles.addStyleClass(toolbar, RIGHT, TOP, BOTTOM, LEFT);
toolbar.getItems().setAll(toolBarButtons(VERTICAL));
borderPane.setRight(toolbar);
}
case BOTTOM -> {
toolbar.setOrientation(HORIZONTAL);
Styles.addStyleClass(toolbar, BOTTOM, TOP, RIGHT, LEFT);
toolbar.getItems().setAll(toolBarButtons(HORIZONTAL));
borderPane.setBottom(toolbar);
}
case LEFT -> {
toolbar.setOrientation(VERTICAL);
Styles.addStyleClass(toolbar, LEFT, RIGHT, TOP, BOTTOM);
toolbar.getItems().setAll(toolBarButtons(VERTICAL));
borderPane.setLeft(toolbar);
}
}
});
}
public Node[] toolBarButtons(Orientation orientation) {
var result = new ArrayList<Node>();
result.add(iconButton(Feather.FILE, false));
result.add(iconButton(Feather.FOLDER, false));
result.add(iconButton(Feather.SAVE, false));
result.add(new Separator());
if (orientation == HORIZONTAL) {
result.add(button("Undo", null, false));
result.add(button("Redo", null, true));
result.add(new Separator());
var group = new ToggleGroup();
result.add(toggleButton("", Feather.BOLD, null, true, BUTTON_ICON, LEFT_PILL));
result.add(toggleButton("", Feather.ITALIC, null, false, BUTTON_ICON, CENTER_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), menuItems(5));
settingsMenu.getStyleClass().add(FLAT);
result.add(new Spacer());
result.add(settingsMenu);
}
if (orientation == VERTICAL) {
result.add(iconButton(Feather.CORNER_DOWN_LEFT, false));
result.add(iconButton(Feather.CORNER_DOWN_RIGHT, true));
result.add(new Spacer(orientation));
result.add(iconButton(Feather.SETTINGS, false));
}
return result.toArray(Node[]::new);
}
private Label gridLabel(String text) {
var label = new Label(text);
label.setAlignment(Pos.CENTER_RIGHT);
label.setMaxWidth(Double.MAX_VALUE);
return label;
}
public static MenuItem[] menuItems(int count) {
return IntStream.range(0, count).mapToObj(i -> new MenuItem(FAKER.babylon5().character())).toArray(MenuItem[]::new);
}
///////////////////////////////////////////////////////////////////////////
public static class TopBar extends VBox {
private static final Region DUMMY_MENUBAR = new Region();
private static final Region DUMMY_TOOLBAR = new Region();
public TopBar(ToolBar toolBar) {
super();
getChildren().setAll(DUMMY_MENUBAR, toolBar);
}
public void showOrCreateMenuBar() {
if (getChildren().get(0) instanceof MenuBar menuBar) {
menuBar.setVisible(true);
menuBar.setManaged(true);
} else {
getChildren().set(0, new SampleMenuBar(FAKER));
}
}
public void hideMenuBar() {
var any = getChildren().get(0);
any.setVisible(false);
any.setManaged(false);
}
public void setToolBar(ToolBar toolBar) {
getChildren().set(1, toolBar);
}
public void removeToolBar() {
getChildren().set(1, DUMMY_TOOLBAR);
}
}
}

@ -0,0 +1,103 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.FlowPane;
import javafx.stage.PopupWindow.AnchorLocation;
import javafx.util.Duration;
import static javafx.geometry.Orientation.VERTICAL;
public class TooltipPage extends AbstractPage {
public static final String NAME = "Tooltip";
@Override
public String getName() { return NAME; }
public TooltipPage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(
basicSamples(),
positionSamples().getRoot()
);
}
private FlowPane basicSamples() {
var basicTooltip = new Tooltip(FAKER.harryPotter().spell());
basicTooltip.setHideDelay(Duration.seconds(3));
var basicLabel = label("Hover me");
basicLabel.setTooltip(basicTooltip);
var basicBlock = new SampleBlock("Basic", basicLabel);
var textWrapTooltip = new Tooltip(FAKER.lorem().paragraph(5));
textWrapTooltip.setHideDelay(Duration.seconds(3));
textWrapTooltip.setPrefWidth(200);
textWrapTooltip.setWrapText(true);
var textWrapLabel = label("Hover me");
textWrapLabel.setTooltip(textWrapTooltip);
var textWrapBlock = new SampleBlock("Text wrapping", textWrapLabel);
var indefiniteTooltip = new Tooltip(FAKER.harryPotter().spell());
indefiniteTooltip.setHideDelay(Duration.INDEFINITE);
var indefiniteLabel = label("Hover me");
indefiniteLabel.setTooltip(basicTooltip);
var indefiniteBlock = new SampleBlock("Indefinite", indefiniteLabel);
return new FlowPane(20, 10,
basicBlock.getRoot(),
textWrapBlock.getRoot(),
indefiniteBlock.getRoot()
);
}
private SampleBlock positionSamples() {
var topLeftLabel = label("Top Left");
topLeftLabel.setTooltip(tooltip("Top Left", AnchorLocation.WINDOW_BOTTOM_RIGHT));
var topRightLabel = label("Top Right");
topRightLabel.setTooltip(tooltip("Top Right", AnchorLocation.WINDOW_BOTTOM_LEFT));
var bottomLeftLabel = label("Bottom Left");
bottomLeftLabel.setTooltip(tooltip("Bottom Left", AnchorLocation.WINDOW_TOP_RIGHT));
var bottomRightLabel = label("Bottom Right");
bottomRightLabel.setTooltip(tooltip("Bottom Right", AnchorLocation.WINDOW_TOP_LEFT));
var flowPane = new FlowPane(20, 10);
flowPane.getChildren().setAll(
topLeftLabel,
new Separator(VERTICAL),
topRightLabel,
new Separator(VERTICAL),
bottomLeftLabel,
new Separator(VERTICAL),
bottomRightLabel
);
return new SampleBlock("Position", flowPane);
}
private Label label(String text) {
Label label = new Label(text);
label.setMinWidth(50);
label.setMinHeight(50);
label.setAlignment(Pos.CENTER_LEFT);
return label;
}
private Tooltip tooltip(String text, AnchorLocation anchorLocation) {
var tooltip = new Tooltip(text);
tooltip.setAnchorLocation(anchorLocation);
return tooltip;
}
}

@ -0,0 +1,282 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.page.AbstractPage;
import javafx.geometry.Orientation;
import javafx.scene.control.*;
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.HBox;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
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 VBox playground;
private ComboBox<Example> exampleSelect;
public TreePage() {
super();
createView();
}
private void createView() {
exampleSelect = exampleSelect();
playground = playground(exampleSelect);
userContent.getChildren().setAll(playground);
}
@Override
protected void onRendered() {
super.onRendered();
exampleSelect.getSelectionModel().selectFirst();
}
private VBox playground(ComboBox<Example> exampleSelect) {
var playground = new VBox(10);
playground.setMinHeight(100);
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 disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> findDisplayedTree().ifPresent(tv -> {
if (val != null) { tv.setDisable(val); }
}));
var controls = new HBox(20,
new Spacer(),
showRootToggle,
disableToggle,
new Spacer()
);
playground.getChildren().setAll(
new Label("Select an example:"),
exampleSelect,
new Spacer(Orientation.VERTICAL), // placeholder for TreeView<?>
controls
);
return playground;
}
private ComboBox<Example> exampleSelect() {
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; }
if (playground.getChildren().size() != 4) {
throw new RuntimeException("Unexpected container size.");
}
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());
});
playground.getChildren().set(2, 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;
}
private Optional<TreeView<?>> findDisplayedTree() {
if (playground == null) { return Optional.empty(); }
return playground.getChildren().stream()
.filter(c -> c instanceof TreeView<?>)
.findFirst()
.map(c -> (TreeView<?>) c);
}
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);
}
}
}

@ -0,0 +1,236 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.CaptionMenuItem;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.sampler.fake.domain.Product;
import atlantafx.sampler.page.AbstractPage;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.cell.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import java.util.List;
import java.util.stream.IntStream;
import static atlantafx.base.theme.Styles.*;
public class TreeTablePage extends AbstractPage {
public static final String NAME = "TreeTableView";
@Override
public String getName() { return NAME; }
private TreeTableView<Product> treeTable;
public TreeTablePage() {
super();
createView();
}
private void createView() {
userContent.getChildren().setAll(
playground()
);
}
private VBox playground() {
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 disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { treeTable.setDisable(val); }
});
var togglesBox = new HBox(20,
bordersToggle,
denseToggle,
stripesToggle,
disableToggle
);
togglesBox.setAlignment(Pos.CENTER_LEFT);
// ~
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(
treeItems(idx * 100, FAKER.random().nextInt(5, 10), brand)
);
root.getChildren().add(group);
}
treeTable = treeTable();
treeTable.setRoot(root);
// ~
var topBox = new HBox(
new Label("Example:"),
new Spacer(),
settingsMenu(treeTable)
);
topBox.setAlignment(Pos.CENTER_LEFT);
var playground = new VBox(10);
playground.setMinHeight(100);
playground.getChildren().setAll(
topBox,
treeTable,
togglesBox
);
return playground;
}
private List<TreeItem<Product>> treeItems(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> treeTable() {
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 settingsMenu(TreeTableView<Product> treeTable) {
var resizePolicyCaption = new CaptionMenuItem("Resize Policy");
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);
}
});
var unconstrainedResizeItem = new RadioMenuItem("Unconstrained");
unconstrainedResizeItem.setToggleGroup(resizePolicyGroup);
unconstrainedResizeItem.setUserData(TreeTableView.UNCONSTRAINED_RESIZE_POLICY);
unconstrainedResizeItem.setSelected(true);
var constrainedResizeItem = new RadioMenuItem("Constrained");
constrainedResizeItem.setToggleGroup(resizePolicyGroup);
constrainedResizeItem.setUserData(TreeTableView.CONSTRAINED_RESIZE_POLICY);
// ~
var selectionModeCaption = new CaptionMenuItem("Selection Mode");
var selectionModeGroup = new ToggleGroup();
selectionModeGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof SelectionMode mode) {
treeTable.getSelectionModel().setSelectionMode(mode);
}
});
var singleSelectionItem = new RadioMenuItem("Single");
singleSelectionItem.setToggleGroup(selectionModeGroup);
singleSelectionItem.setUserData(SelectionMode.SINGLE);
var multiSelectionItem = new RadioMenuItem("Multiple");
multiSelectionItem.setToggleGroup(selectionModeGroup);
multiSelectionItem.setUserData(SelectionMode.MULTIPLE);
multiSelectionItem.setSelected(true);
// ~
var showRootItem = new CheckMenuItem("Show root");
treeTable.showRootProperty().bind(showRootItem.selectedProperty());
showRootItem.setSelected(true);
var editCellsItem = new CheckMenuItem("Editable");
treeTable.editableProperty().bind(editCellsItem.selectedProperty());
editCellsItem.setSelected(true);
var cellSelectionItem = new CheckMenuItem("Enable cell selection");
treeTable.getSelectionModel().cellSelectionEnabledProperty().bind(cellSelectionItem.selectedProperty());
cellSelectionItem.setSelected(false);
var menuButtonItem = new CheckMenuItem("Show menu button");
treeTable.tableMenuButtonVisibleProperty().bind(menuButtonItem.selectedProperty());
menuButtonItem.setSelected(true);
return new MenuButton("Settings") {{
getItems().setAll(
resizePolicyCaption,
unconstrainedResizeItem,
constrainedResizeItem,
selectionModeCaption,
singleSelectionItem,
multiSelectionItem,
new SeparatorMenuItem(),
showRootItem,
editCellsItem,
cellSelectionItem,
menuButtonItem
);
}};
}
}

@ -0,0 +1,108 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.general;
import atlantafx.base.theme.Theme;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.theme.ThemeManager;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.util.StringConverter;
import java.util.Objects;
public class ThemePage extends AbstractPage {
public static final String NAME = "Theme";
@Override
public String getName() { return NAME; }
public ThemePage() {
super();
createView();
}
private void createView() {
userContent.getChildren().add(optionsGrid());
sourceCodeToggleBtn.setVisible(false);
}
private GridPane optionsGrid() {
ChoiceBox<Theme> themeSelector = themeSelector();
themeSelector.setPrefWidth(200);
Spinner<Integer> fontSizeSpinner = fontSizeSpinner();
fontSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
fontSizeSpinner.setPrefWidth(200);
// ~
var grid = new GridPane();
grid.setVgap(20);
grid.setHgap(20);
grid.add(new Label("Color theme"), 0, 0);
grid.add(themeSelector, 1, 0);
grid.add(new Label("Font size"), 0, 1);
grid.add(fontSizeSpinner, 1, 1);
return grid;
}
private ChoiceBox<Theme> themeSelector() {
var manager = ThemeManager.getInstance();
var selector = new ChoiceBox<Theme>();
selector.getItems().setAll(manager.getAvailableThemes());
selector.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val != null && getScene() != null) {
ThemeManager.getInstance().setTheme(getScene(), val);
}
});
selector.setConverter(new StringConverter<>() {
@Override
public String toString(Theme theme) {
return theme.getName();
}
@Override
public Theme fromString(String themeName) {
return manager.getAvailableThemes().stream().filter(t -> Objects.equals(themeName, t.getName()))
.findFirst()
.orElse(null);
}
});
// select current theme
if (manager.getTheme() != null) {
selector.getItems().stream()
.filter(t -> Objects.equals(manager.getTheme().getName(), t.getName()))
.findFirst()
.ifPresent(t -> selector.getSelectionModel().select(t));
}
return selector;
}
private Spinner<Integer> fontSizeSpinner() {
var spinner = new Spinner<Integer>(10, 24, 14);
// 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(ThemeManager.getInstance().getFontSize());
spinner.valueProperty().addListener((obs, old, val) -> {
if (val != null && getScene() != null) {
ThemeManager.getInstance().setFontSize(getScene(), val);
}
});
return spinner;
}
}

@ -0,0 +1,160 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.general;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos;
import javafx.scene.Node;
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;
import static atlantafx.base.theme.Styles.*;
public class TypographyPage extends AbstractPage {
public static final String NAME = "Typography";
@Override
public String getName() { return NAME; }
private GridPane fontSizeBox;
public TypographyPage() {
super();
createView();
}
private void createView() {
var fontSizeSample = fontSizeSample();
fontSizeBox = (GridPane) fontSizeSample.getContent();
userContent.getChildren().setAll(
fontSizeSample.getRoot(),
fontWeightSample().getRoot(),
fontStyleSample().getRoot(),
hyperlinkSample().getRoot(),
textColorSample().getRoot(),
textFlowSample().getRoot()
);
}
private SampleBlock fontSizeSample() {
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(10);
grid.add(text("Title 1", TITLE_1), 0, 0);
grid.add(text("Title 2", TITLE_2), 0, 1);
grid.add(text("Title 3", TITLE_3), 0, 2);
grid.add(text("Title 4", TITLE_4), 0, 3);
grid.add(text("Caption", TEXT_CAPTION), 1, 0);
grid.add(text("Default"), 1, 1);
grid.add(text("Small", TEXT_SMALL), 1, 2);
grid.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Font size", grid);
}
private SampleBlock fontWeightSample() {
var box = new HBox(10,
text("Bold", TEXT_BOLD),
text("Bolder", TEXT_BOLDER),
text("Normal", TEXT_NORMAL),
text("Lighter", TEXT_LIGHTER)
);
box.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Font weight", box);
}
private SampleBlock fontStyleSample() {
var box = new HBox(10,
text("Italic", TEXT_ITALIC),
text("Oblique", TEXT_OBLIQUE),
text("Underlined", TEXT_UNDERLINED),
text("Strikethrough", TEXT_STRIKETHROUGH)
);
box.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Font style", box);
}
private SampleBlock textColorSample() {
var box = new HBox(10,
text("Accent", TEXT, ACCENT),
text("Success", TEXT, SUCCESS),
text("Warning", TEXT, WARNING),
text("Danger", TEXT, DANGER)
);
box.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Text color", box);
}
private SampleBlock hyperlinkSample() {
var linkNormal = hyperlink("_Normal", false, false);
linkNormal.setMnemonicParsing(true);
var linkVisited = hyperlink("_Visited", true, false);
linkVisited.setMnemonicParsing(true);
var box = new HBox(10,
linkNormal,
linkVisited,
hyperlink("Disabled", false, true)
);
box.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Hyperlink", box);
}
private Text text(String text, String... styleClasses) {
var t = new Text(text);
t.getStyleClass().addAll(styleClasses);
return t;
}
private Hyperlink hyperlink(String text, boolean visited, boolean disabled) {
var h = new Hyperlink(text);
h.setVisited(visited);
h.setDisable(disabled);
return h;
}
private SampleBlock textFlowSample() {
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);
}
// font metrics can only be obtained by requesting from a rendered node
protected void onRendered() {
for (Node node : fontSizeBox.getChildren()) {
if (node instanceof Text textNode) {
var font = textNode.getFont();
textNode.setText(String.format("%s = %.1fpx",
textNode.getText(),
Math.ceil(font.getSize())
));
}
}
}
}

@ -0,0 +1,22 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase;
import atlantafx.sampler.page.AbstractPage;
public class Chat extends AbstractPage {
public static final String NAME = "Chat";
public Chat() {
super();
createView();
}
private void createView() {
}
@Override
public String getName() {
return NAME;
}
}

@ -0,0 +1,22 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase;
import atlantafx.sampler.page.AbstractPage;
public class FileManager extends AbstractPage {
public static final String NAME = "File Manager";
public FileManager() {
super();
createView();
}
private void createView() {
}
@Override
public String getName() {
return NAME;
}
}

@ -0,0 +1,22 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase;
import atlantafx.sampler.page.AbstractPage;
public class MusicPlayer extends AbstractPage {
public static final String NAME = "Music Player";
public MusicPlayer() {
super();
createView();
}
private void createView() {
}
@Override
public String getName() {
return NAME;
}
}

@ -0,0 +1,22 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase;
import atlantafx.sampler.page.AbstractPage;
public class TextEditor extends AbstractPage {
public static final String NAME = "Text Editor";
public TextEditor() {
super();
createView();
}
private void createView() {
}
@Override
public String getName() {
return NAME;
}
}

@ -0,0 +1,38 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.theme;
import atlantafx.base.theme.AbstractTheme;
import java.net.URI;
import java.util.Objects;
import java.util.Set;
public class ExternalTheme extends AbstractTheme {
private final String name;
private final String stylesheet;
private final boolean darkMode;
public ExternalTheme(String name, String stylesheet, Set<URI> stylesheets, boolean darkMode) {
super(stylesheets);
this.name = Objects.requireNonNull(name);
this.stylesheet = Objects.requireNonNull(stylesheet);
this.darkMode = darkMode;
}
@Override
public String getName() {
return name;
}
@Override
public String getUserAgentStylesheet() {
return stylesheet;
}
@Override
public boolean isDarkMode() {
return darkMode;
}
}

@ -0,0 +1,33 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.theme;
public class HighlightJSTheme {
private final String css;
private final String background;
public HighlightJSTheme(String css, String background) {
this.css = css;
this.background = background;
}
public String getCss() {
return css;
}
public String getBackground() {
return background;
}
public static HighlightJSTheme githubLight() {
var css = ".hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}";
var bg = "#fff";
return new HighlightJSTheme(css, bg);
}
public static HighlightJSTheme githubDark() {
var css = ".hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}";
var bg = "#0d1117";
return new HighlightJSTheme(css, bg);
}
}

@ -0,0 +1,111 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.theme;
import atlantafx.sampler.Resources;
import atlantafx.base.theme.PrimerDark;
import atlantafx.base.theme.PrimerLight;
import atlantafx.base.theme.Theme;
import atlantafx.sampler.Launcher;
import javafx.application.Application;
import javafx.scene.Scene;
import java.net.URI;
import java.util.*;
import static java.nio.charset.StandardCharsets.UTF_8;
public final class ThemeManager {
private static final String DUMMY_STYLESHEET = Resources.getResource("assets/styles/empty.css").toString();
private Theme currentTheme = null;
private int currentFontSize = 14;
public Theme getTheme() {
return currentTheme;
}
/**
* Resets user agent stylesheet and then adds {@link Theme} styles to the {@link Scene}
* stylesheets. This is necessary when we want to reload style changes at runtime, because
* CSSFX doesn't monitor user agent stylesheet.
* Also, some styles aren't applied when using {@link Application#setUserAgentStylesheet(String)} ).
* E.g. JavaFX ignores Ikonli -fx-icon-color and -fx-icon-size properties, but for an unknown
* reason they won't be ignored when exactly the same stylesheet is set via {@link Scene#getStylesheets()}.
*/
public void setTheme(Scene scene, Theme theme) {
Objects.requireNonNull(theme);
Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet()));
if (currentTheme != null) {
scene.getStylesheets().removeIf(url -> currentTheme.getStylesheets().contains(URI.create(url)));
}
theme.getStylesheets().forEach(uri -> scene.getStylesheets().add(uri.toString()));
currentTheme = theme;
}
public List<Theme> getAvailableThemes() {
var themes = new ArrayList<Theme>();
var appStylesheets = new URI[] {
URI.create(Resources.resolve("assets/fonts/index.css")),
URI.create(Resources.resolve("assets/styles/index.css"))
};
if (Launcher.IS_DEV_MODE) {
themes.add(new ExternalTheme("Primer Light", DUMMY_STYLESHEET, merge(
Resources.getResource("theme-test/primer-light.css"),
appStylesheets
), false));
themes.add(new ExternalTheme("Primer Dark", DUMMY_STYLESHEET, merge(
Resources.getResource("theme-test/primer-dark.css"),
appStylesheets
), true));
} else {
themes.add(new PrimerLight(appStylesheets));
themes.add(new PrimerDark(appStylesheets));
}
return themes;
}
public int getFontSize() {
return currentFontSize;
}
public void setFontSize(Scene scene, int fontSize) {
String css = String.format(".root { -fx-font-size: %dpx; } .ikonli-font-icon { -fx-icon-size: %dpx; }",
fontSize,
fontSize + 2
);
scene.getStylesheets().removeIf(uri -> uri.startsWith("data:text/css"));
scene.getStylesheets().add(
"data:text/css;base64," + Base64.getEncoder().encodeToString(css.getBytes(UTF_8))
);
currentFontSize = fontSize;
}
@SafeVarargs
private <T> Set<T> merge(T first, T... arr) {
var set = new LinkedHashSet<T>();
set.add(first);
Collections.addAll(set, arr);
return set;
}
///////////////////////////////////////////////////////////////////////////
// Singleton //
///////////////////////////////////////////////////////////////////////////
private ThemeManager() { }
private static class InstanceHolder {
private static final ThemeManager INSTANCE = new ThemeManager();
}
public static ThemeManager getInstance() {
return InstanceHolder.INSTANCE;
}
}

@ -0,0 +1,44 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.util;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
public final class Containers {
public static final ColumnConstraints H_GROW_NEVER = columnConstraints(Priority.NEVER);
public static void setAnchors(Parent parent, Insets insets) {
if (insets.getTop() >= 0) { AnchorPane.setTopAnchor(parent, insets.getTop()); }
if (insets.getRight() >= 0) { AnchorPane.setRightAnchor(parent, insets.getRight()); }
if (insets.getBottom() >= 0) { AnchorPane.setBottomAnchor(parent, insets.getBottom()); }
if (insets.getLeft() >= 0) { AnchorPane.setLeftAnchor(parent, insets.getLeft()); }
}
public static void setScrollConstraints(ScrollPane scrollPane,
ScrollPane.ScrollBarPolicy vbarPolicy, boolean fitHeight,
ScrollPane.ScrollBarPolicy hbarPolicy, boolean fitWidth) {
scrollPane.setVbarPolicy(vbarPolicy);
scrollPane.setFitToHeight(fitHeight);
scrollPane.setHbarPolicy(hbarPolicy);
scrollPane.setFitToWidth(fitWidth);
}
public static ColumnConstraints columnConstraints(Priority hgrow) {
return columnConstraints(USE_COMPUTED_SIZE, hgrow);
}
public static ColumnConstraints columnConstraints(double minWidth, Priority hgrow) {
double maxWidth = hgrow == Priority.ALWAYS ? Double.MAX_VALUE : USE_PREF_SIZE;
ColumnConstraints constraints = new ColumnConstraints(minWidth, USE_COMPUTED_SIZE, maxWidth);
constraints.setHgrow(hgrow);
return constraints;
}
}

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