commit 5169b3de7bd94345c3527b116887244a9a5b3a25 Author: mkpaz Date: Thu Jul 21 12:58:01 2022 +0400 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/actions/prepare-java/action.yml b/.github/actions/prepare-java/action.yml new file mode 100644 index 0000000..cb5d433 --- /dev/null +++ b/.github/actions/prepare-java/action.yml @@ -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 diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml new file mode 100644 index 0000000..1d092a4 --- /dev/null +++ b/.github/workflows/tagged-release.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..e976676 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..4f3b9f5 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -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 diff --git a/.screenshots/primer_dark.png b/.screenshots/primer_dark.png new file mode 100644 index 0000000..20585f7 Binary files /dev/null and b/.screenshots/primer_dark.png differ diff --git a/.screenshots/primer_light.png b/.screenshots/primer_light.png new file mode 100644 index 0000000..4e5f32c Binary files /dev/null and b/.screenshots/primer_light.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1232da4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [Unreleased] + +## [0.1] + +Initial release. diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..994d1fc --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb9c518 --- /dev/null +++ b/README.md @@ -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. diff --git a/base/pom.xml b/base/pom.xml new file mode 100755 index 0000000..d113a98 --- /dev/null +++ b/base/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + io.github.mkpaz + atlantafx-parent + 0.1.0 + + atlantafx-base + + + + ${project.groupId} + atlantafx-styles + + + org.openjfx + javafx-controls + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + + + org.assertj + assertj-core + + + + + + + + src/main/resources + atlantafx/base + false + + + + ../styles/dist + atlantafx/base/theme + false + + + + + + + com.github.eirslett + frontend-maven-plugin + + ${project.parent.basedir} + + + + + + diff --git a/base/src/main/java/atlantafx/base/controls/BehaviorBase.java b/base/src/main/java/atlantafx/base/controls/BehaviorBase.java new file mode 100755 index 0000000..a1be0ff --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/BehaviorBase.java @@ -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> { + + 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; + } +} diff --git a/base/src/main/java/atlantafx/base/controls/BehaviorSkinBase.java b/base/src/main/java/atlantafx/base/controls/BehaviorSkinBase.java new file mode 100755 index 0000000..fc8e473 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/BehaviorSkinBase.java @@ -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> extends SkinBase { + + 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; + } + } +} diff --git a/base/src/main/java/atlantafx/base/controls/Breadcrumbs.java b/base/src/main/java/atlantafx/base/controls/Breadcrumbs.java new file mode 100755 index 0000000..9be791f --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/Breadcrumbs.java @@ -0,0 +1,359 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2014, 2020, ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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 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 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> CRUMB_ACTION + = new EventType<>("CRUMB_ACTION" + UUID.randomUUID()); + + private final TreeItem selectedCrumb; + + /** Creates a new event that can subsequently be fired. */ + public BreadCrumbActionEvent(TreeItem selectedCrumb) { + super(CRUMB_ACTION); + this.selectedCrumb = selectedCrumb; + } + + /** Returns the crumb which was the action target. */ + public TreeItem 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 TreeItem buildTreeModel(T... crumbs) { + TreeItem subRoot = null; + for (T crumb : crumbs) { + TreeItem 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, 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 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. + * + *

+ * Consider the following hierarchy: + * [Root] > [Folder] > [SubFolder] > [file.txt] + *

+ * To show the above bread crumb bar, you have to set the [file.txt] tree-node as selected crumb. + */ + public final ObjectProperty> selectedCrumbProperty() { + return selectedCrumb; + } + + private final ObjectProperty> selectedCrumb = + new SimpleObjectProperty<>(this, "selectedCrumb"); + + /** Get the current target path. */ + public final TreeItem getSelectedCrumb() { + return selectedCrumb.get(); + } + + /** Select one node in the BreadCrumbBar for being the bottom-most path node. */ + public final void setSelectedCrumb(TreeItem 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, Button>> crumbFactoryProperty() { + return crumbFactory; + } + + private final ObjectProperty, Button>> crumbFactory = + new SimpleObjectProperty<>(this, "crumbFactory"); + + /** + * Sets the crumb factory to create (custom) {@link BreadCrumbButton} instances. + * null is not allowed and will result in a fallback to the default factory. + */ + public final void setCrumbFactory(Callback, 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, Button> getCrumbFactory() { + return crumbFactory.get(); + } + + /** + * @return an ObjectProperty representing the crumbAction EventHandler being used. + */ + public final ObjectProperty>> onCrumbActionProperty() { + return onCrumbAction; + } + + /** Set a new EventHandler for when a user selects a crumb. */ + public final void setOnCrumbAction(EventHandler> 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> getOnCrumbAction() { + return onCrumbActionProperty().get(); + } + + private final ObjectProperty>> onCrumbAction = new ObjectPropertyBase<>() { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + protected void invalidated() { + setEventHandler(BreadCrumbActionEvent.CRUMB_ACTION, (EventHandler) (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 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; + } + } +} diff --git a/base/src/main/java/atlantafx/base/controls/BreadcrumbsSkin.java b/base/src/main/java/atlantafx/base/controls/BreadcrumbsSkin.java new file mode 100755 index 0000000..e0f9193 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/BreadcrumbsSkin.java @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2014, 2021, ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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 extends SkinBase> { + + private static final String STYLE_CLASS_FIRST = "first"; + + public BreadcrumbsSkin(final Breadcrumbs control) { + super(control); + control.selectedCrumbProperty().addListener(selectedPathChangeListener); + updateSelectedPath(getSkinnable().selectedCrumbProperty().get(), null); + } + + @SuppressWarnings("FieldCanBeLocal") + private final ChangeListener> selectedPathChangeListener = + (obs, oldItem, newItem) -> updateSelectedPath(newItem, oldItem); + + private void updateSelectedPath(TreeItem newTarget, TreeItem 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> treeChildrenModifiedHandler = + args -> updateBreadCrumbs(); + + private void updateBreadCrumbs() { + final Breadcrumbs buttonBar = getSkinnable(); + final TreeItem pathTarget = buttonBar.getSelectedCrumb(); + final Callback, Button> factory = buttonBar.getCrumbFactory(); + + getChildren().clear(); + + if (pathTarget != null) { + List> 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> constructFlatPath(TreeItem bottomMost) { + List> path = new ArrayList<>(); + + TreeItem current = bottomMost; + do { + path.add(current); + current = current.getParent(); + } while (current != null); + + Collections.reverse(path); + return path; + } + + private Button createCrumb( + final Callback, Button> factory, + final TreeItem 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 crumbModel) { + final Breadcrumbs 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); + } + } +} diff --git a/base/src/main/java/atlantafx/base/controls/CaptionMenuItem.java b/base/src/main/java/atlantafx/base/controls/CaptionMenuItem.java new file mode 100644 index 0000000..c7825fc --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/CaptionMenuItem.java @@ -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(); + } +} diff --git a/base/src/main/java/atlantafx/base/controls/CustomTextField.java b/base/src/main/java/atlantafx/base/controls/CustomTextField.java new file mode 100755 index 0000000..16dfa2a --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/CustomTextField.java @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2013, 2015, ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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 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 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 right = new SimpleObjectProperty<>(this, "right"); + + /** + * Property representing the {@link Node} that is placed on the right of the text field. + */ + public final ObjectProperty 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 leftProperty() { + return CustomTextField.this.leftProperty(); + } + + @Override + public ObjectProperty rightProperty() { + return CustomTextField.this.rightProperty(); + } + }; + } +} diff --git a/base/src/main/java/atlantafx/base/controls/CustomTextFieldSkin.java b/base/src/main/java/atlantafx/base/controls/CustomTextFieldSkin.java new file mode 100755 index 0000000..e961c9f --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/CustomTextFieldSkin.java @@ -0,0 +1,173 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2013, 2019 ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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 leftProperty(); + + public abstract ObjectProperty 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)); + } +} diff --git a/base/src/main/java/atlantafx/base/controls/InlineDatePicker.java b/base/src/main/java/atlantafx/base/controls/InlineDatePicker.java new file mode 100755 index 0000000..ce15316 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/InlineDatePicker.java @@ -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. + *

+ * The {@link #valueProperty() value} property represents the currently selected + * {@link LocalDate}. The default value is null. + *

+ * The {@link #chronologyProperty() chronology} property specifies a calendar system to be used + * for parsing, displaying, and choosing dates. + *

+ * 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 null 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 value = new SimpleObjectProperty<>(this, "value"); + + public final LocalDate getValue() { + return valueProperty().get(); + } + + public final void setValue(LocalDate value) { + valueProperty().set(value); + } + + public ObjectProperty 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> dayCellFactory; + + public final void setDayCellFactory(Callback value) { + dayCellFactoryProperty().set(value); + } + + public final Callback getDayCellFactory() { + return (dayCellFactory != null) ? dayCellFactory.get() : null; + } + + public final ObjectProperty> 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. + *

+ * The default is usually {@link IsoChronology} unless provided explicitly + * in the {@link Locale} by use of a Locale calendar extension. + *

+ * Setting the value to null will restore the default chronology. + * + * @return a property representing the Chronology being used + */ + public ObjectProperty chronologyProperty() { + return chronology; + } + + private final ObjectProperty 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. + *

+ * 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 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> STYLEABLES; + + private static final CssMetaData 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 getStyleableProperty(InlineDatePicker n) { + return (StyleableProperty) (WritableValue) n.showWeekNumbersProperty(); + } + }; + + static { + final List> 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> getClassCssMetaData() { + return StyleableProperties.STYLEABLES; + } + + /** {@inheritDoc} */ + @Override + public List> 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; + } + } +} diff --git a/base/src/main/java/atlantafx/base/controls/InlineDatePickerBehavior.java b/base/src/main/java/atlantafx/base/controls/InlineDatePickerBehavior.java new file mode 100755 index 0000000..d4a948a --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/InlineDatePickerBehavior.java @@ -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 { + + 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(); + } +} diff --git a/base/src/main/java/atlantafx/base/controls/InlineDatePickerSkin.java b/base/src/main/java/atlantafx/base/controls/InlineDatePickerSkin.java new file mode 100755 index 0000000..f4ef86a --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/InlineDatePickerSkin.java @@ -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 { + + // 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 dayNameCells = new ArrayList<>(); + protected final List weekNumberCells = new ArrayList<>(); + protected final List dayCells = new ArrayList<>(); + protected LocalDate[] dayCellDates; + protected DateCell lastFocusedDayCell = null; + protected final int daysPerWeek = getDaysPerWeek(); + + private final ObjectProperty displayedYearMonth = new SimpleObjectProperty<>(this, "displayedYearMonth"); + + public ObjectProperty displayedYearMonthProperty() { return displayedYearMonth; } + + private final ObjectBinding 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 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 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(); + } +} diff --git a/base/src/main/java/atlantafx/base/controls/Popover.java b/base/src/main/java/atlantafx/base/controls/Popover.java new file mode 100755 index 0000000..cc9fa0f --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/Popover.java @@ -0,0 +1,920 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2013, 2022 ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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. + *

+ * 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 fadeInDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION); + private final ObjectProperty 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: + * + *

+     * Popover popOver = new Popover();
+     * popOver.getRoot().getStylesheets().add(...);
+     * 
+ * + * @return the root pane + */ + public final StackPane getRoot() { + return root; + } + + private final ObjectProperty 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 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 xListener = (value, oldX, newX) -> { + if (!isDetached()) { + setAnchorX(getAnchorX() + (newX.doubleValue() - oldX.doubleValue())); + } + }; + + private final WeakChangeListener weakXListener = new WeakChangeListener<>(xListener); + + private final ChangeListener yListener = (value, oldY, newY) -> { + if (!isDetached()) { + setAnchorY(getAnchorY() + (newY.doubleValue() - oldY.doubleValue())); + } + }; + + private final WeakChangeListener weakYListener = new WeakChangeListener<>(yListener); + + private Window ownerWindow; + private final EventHandler closePopoverOnOwnerWindowCloseLambda = event -> ownerWindowHiding(); + private final WeakEventHandler 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 = 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 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 fadeInDurationProperty() { + return fadeInDuration; + } + + /** + * Stores the fade-out duration. + * + * @return the fade-out duration property + */ + public final ObjectProperty 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); + } +} diff --git a/base/src/main/java/atlantafx/base/controls/PopoverSkin.java b/base/src/main/java/atlantafx/base/controls/PopoverSkin.java new file mode 100755 index 0000000..d96b4f8 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/PopoverSkin.java @@ -0,0 +1,667 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2013 - 2015, ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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 { + + 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 mousePressedHandler = evt -> { + if (popover.isDetachable() || popover.isDetached()) { + tornOff = false; + + xOffset = evt.getScreenX(); + yOffset = evt.getScreenY(); + + dragStartLocation = new Point2D(xOffset, yOffset); + } + }; + + final EventHandler mouseReleasedHandler = evt -> { + if (tornOff && !getSkinnable().isDetached()) { + tornOff = false; + getSkinnable().detach(); + } + }; + + final EventHandler 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 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); + } +} diff --git a/base/src/main/java/atlantafx/base/controls/Spacer.java b/base/src/main/java/atlantafx/base/controls/Spacer.java new file mode 100644 index 0000000..f826e73 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/Spacer.java @@ -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); + } + } + } +} diff --git a/base/src/main/java/atlantafx/base/controls/ToggleSwitch.java b/base/src/main/java/atlantafx/base/controls/ToggleSwitch.java new file mode 100755 index 0000000..67a4d94 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/ToggleSwitch.java @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2015, ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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); + } +} diff --git a/base/src/main/java/atlantafx/base/controls/ToggleSwitchSkin.java b/base/src/main/java/atlantafx/base/controls/ToggleSwitchSkin.java new file mode 100755 index 0000000..9c3b972 --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/ToggleSwitchSkin.java @@ -0,0 +1,249 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (c) 2015, 2020, 2021, ControlsFX + * All rights reserved. + *

+ * 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. + *

+ * 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 { + + 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 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 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 getStyleableProperty(ToggleSwitch toggleSwitch) { + final ToggleSwitchSkin skin = (ToggleSwitchSkin) toggleSwitch.getSkin(); + return (StyleableProperty) (WritableValue) skin.thumbMoveAnimationTimeProperty(); + } + }; + + private static final List> STYLEABLES; + + static { + final List> 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> getClassCssMetaData() { + return STYLEABLES; + } + + /** + * {@inheritDoc} + */ + @Override + public List> getCssMetaData() { + return getClassCssMetaData(); + } +} diff --git a/base/src/main/java/atlantafx/base/theme/AbstractTheme.java b/base/src/main/java/atlantafx/base/theme/AbstractTheme.java new file mode 100644 index 0000000..b47f1a6 --- /dev/null +++ b/base/src/main/java/atlantafx/base/theme/AbstractTheme.java @@ -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 stylesheets; + + public AbstractTheme() { + this(new LinkedHashSet<>()); + } + + public AbstractTheme(URI... stylesheets) { + this(Set.of(stylesheets)); + } + + public AbstractTheme(Set stylesheets) { + this.stylesheets = Objects.requireNonNull(stylesheets); + } + + @Override + public Set getStylesheets() { + return stylesheets; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "{" + + "name=" + getName() + + ", userAgentStylesheet=" + getUserAgentStylesheet() + + ", stylesheets=" + stylesheets + + ", isDarkMode=" + isDarkMode() + + '}'; + } +} diff --git a/base/src/main/java/atlantafx/base/theme/PrimerDark.java b/base/src/main/java/atlantafx/base/theme/PrimerDark.java new file mode 100755 index 0000000..139b378 --- /dev/null +++ b/base/src/main/java/atlantafx/base/theme/PrimerDark.java @@ -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 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; + } +} diff --git a/base/src/main/java/atlantafx/base/theme/PrimerLight.java b/base/src/main/java/atlantafx/base/theme/PrimerLight.java new file mode 100755 index 0000000..6922332 --- /dev/null +++ b/base/src/main/java/atlantafx/base/theme/PrimerLight.java @@ -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 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; + } +} diff --git a/base/src/main/java/atlantafx/base/theme/Styles.java b/base/src/main/java/atlantafx/base/theme/Styles.java new file mode 100644 index 0000000..dec8a8c --- /dev/null +++ b/base/src/main/java/atlantafx/base/theme/Styles.java @@ -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); + } +} diff --git a/base/src/main/java/atlantafx/base/theme/Theme.java b/base/src/main/java/atlantafx/base/theme/Theme.java new file mode 100755 index 0000000..86fefd2 --- /dev/null +++ b/base/src/main/java/atlantafx/base/theme/Theme.java @@ -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 getStylesheets(); + + boolean isDarkMode(); + + default boolean isDefault() { + return STYLESHEET_MODENA.equals(getUserAgentStylesheet()) || STYLESHEET_CASPIAN.equals(getUserAgentStylesheet()); + } +} diff --git a/base/src/main/java/atlantafx/base/util/DoubleStringConverter.java b/base/src/main/java/atlantafx/base/util/DoubleStringConverter.java new file mode 100644 index 0000000..a77a357 --- /dev/null +++ b/base/src/main/java/atlantafx/base/util/DoubleStringConverter.java @@ -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. + *

+ * 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.

+ * + * @author Christoph Nahr + * @version 1.0.2 + */ +public class DoubleStringConverter extends StringConverter { + + 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 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 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); + } +} diff --git a/base/src/main/java/atlantafx/base/util/IntegerStringConverter.java b/base/src/main/java/atlantafx/base/util/IntegerStringConverter.java new file mode 100644 index 0000000..2bc5521 --- /dev/null +++ b/base/src/main/java/atlantafx/base/util/IntegerStringConverter.java @@ -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 { + + 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 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 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); + } +} diff --git a/base/src/main/java/atlantafx/base/util/PlatformUtils.java b/base/src/main/java/atlantafx/base/util/PlatformUtils.java new file mode 100755 index 0000000..ed3691e --- /dev/null +++ b/base/src/main/java/atlantafx/base/util/PlatformUtils.java @@ -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; + } +} diff --git a/base/src/main/java/module-info.java b/base/src/main/java/module-info.java new file mode 100755 index 0000000..c586c47 --- /dev/null +++ b/base/src/main/java/module-info.java @@ -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; +} diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..5643201 --- /dev/null +++ b/mvnw @@ -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 "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..8a15b7f --- /dev/null +++ b/mvnw.cmd @@ -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% diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 0000000..bbdca2f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2514 @@ +{ + "name": "atlanta", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "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" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clean-css": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz", + "integrity": "sha512-nKseG8wCzEuji/4yrgM/5cthL9oTDc5UOQyFMvW/Q53oP6gLH690o1NbuTh6Y18nujr7BxlsFuS7gXLnLzKJGg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "dependencies": { + "glob": "~5.0.0" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/findup-sync/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/getobject": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", + "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/grunt": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.4.1.tgz", + "integrity": "sha512-ZXIYXTsAVrA7sM+jZxjQdrBOAg7DyMUplOMhTaspMRExei+fD0BTwdWXnn0W5SXqhb/Q/nlkzXclSi3IH55PIA==", + "dev": true, + "dependencies": { + "dateformat": "~3.0.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.2", + "findup-sync": "~0.3.0", + "glob": "~7.1.6", + "grunt-cli": "~1.4.2", + "grunt-known-options": "~2.0.0", + "grunt-legacy-log": "~3.0.0", + "grunt-legacy-util": "~2.0.1", + "iconv-lite": "~0.4.13", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "mkdirp": "~1.0.4", + "nopt": "~3.0.6", + "rimraf": "~3.0.2" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-cli": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", + "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", + "dev": true, + "dependencies": { + "grunt-known-options": "~2.0.0", + "interpret": "~1.1.0", + "liftup": "~3.0.1", + "nopt": "~4.0.1", + "v8flags": "~3.2.0" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-cli/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/grunt-contrib-concat": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-2.0.0.tgz", + "integrity": "sha512-/cfWwsGiprVTOl7c2bZwMdQ8hIf3e1f4szm1i7qhY9hOnR/X2KL+Xe7dynNweTYHa6aWPZx2B5GPsUpxAXNCaA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "source-map": "^0.5.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "grunt": ">=1.4.1" + } + }, + "node_modules/grunt-contrib-cssmin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-4.0.0.tgz", + "integrity": "sha512-jXU+Zlk8Q8XztOGNGpjYlD/BDQ0n95IHKrQKtFR7Gd8hZrzgqiG1Ra7cGYc8h2DD9vkSFGNlweb9Q00rBxOK2w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "clean-css": "^5.0.1", + "maxmin": "^3.0.0" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/grunt-known-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", + "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", + "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", + "dev": true, + "dependencies": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", + "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", + "dev": true, + "dependencies": { + "chalk": "~4.1.0", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-legacy-util": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", + "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", + "dev": true, + "dependencies": { + "async": "~3.2.0", + "exit": "~0.1.2", + "getobject": "~1.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.21", + "underscore.string": "~3.3.5", + "which": "~2.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-sass": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-3.1.0.tgz", + "integrity": "sha512-90s27H7FoCDcA8C8+R0GwC+ntYD3lG6S/jqcavWm3bn9RiJTmSfOvfbFa1PXx4NbBWuiGQMLfQTj/JvvqT5w6A==", + "dev": true, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "grunt": ">=1" + } + }, + "node_modules/gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/liftup": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", + "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^4.0.0", + "fined": "^1.2.0", + "flagged-respawn": "^1.0.1", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.1", + "rechoir": "^0.7.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/liftup/node_modules/findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maxmin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-3.0.0.tgz", + "integrity": "sha512-wcahMInmGtg/7c6a75fr21Ch/Ks1Tb+Jtoan5Ft4bAI0ZvJqyOw8kkM7e7p8hDSzY805vmxwHT50KcjGwKyJ0g==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "figures": "^3.2.0", + "gzip-size": "^5.1.1", + "pretty-bytes": "^5.3.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.49.9", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", + "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/underscore.string/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "clean-css": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz", + "integrity": "sha512-nKseG8wCzEuji/4yrgM/5cthL9oTDc5UOQyFMvW/Q53oP6gLH690o1NbuTh6Y18nujr7BxlsFuS7gXLnLzKJGg==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "requires": { + "glob": "~5.0.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "getobject": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", + "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", + "dev": true + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "grunt": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.4.1.tgz", + "integrity": "sha512-ZXIYXTsAVrA7sM+jZxjQdrBOAg7DyMUplOMhTaspMRExei+fD0BTwdWXnn0W5SXqhb/Q/nlkzXclSi3IH55PIA==", + "dev": true, + "requires": { + "dateformat": "~3.0.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.2", + "findup-sync": "~0.3.0", + "glob": "~7.1.6", + "grunt-cli": "~1.4.2", + "grunt-known-options": "~2.0.0", + "grunt-legacy-log": "~3.0.0", + "grunt-legacy-util": "~2.0.1", + "iconv-lite": "~0.4.13", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "mkdirp": "~1.0.4", + "nopt": "~3.0.6", + "rimraf": "~3.0.2" + } + }, + "grunt-cli": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", + "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", + "dev": true, + "requires": { + "grunt-known-options": "~2.0.0", + "interpret": "~1.1.0", + "liftup": "~3.0.1", + "nopt": "~4.0.1", + "v8flags": "~3.2.0" + }, + "dependencies": { + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + } + } + }, + "grunt-contrib-concat": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-2.0.0.tgz", + "integrity": "sha512-/cfWwsGiprVTOl7c2bZwMdQ8hIf3e1f4szm1i7qhY9hOnR/X2KL+Xe7dynNweTYHa6aWPZx2B5GPsUpxAXNCaA==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "source-map": "^0.5.3" + } + }, + "grunt-contrib-cssmin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-4.0.0.tgz", + "integrity": "sha512-jXU+Zlk8Q8XztOGNGpjYlD/BDQ0n95IHKrQKtFR7Gd8hZrzgqiG1Ra7cGYc8h2DD9vkSFGNlweb9Q00rBxOK2w==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "clean-css": "^5.0.1", + "maxmin": "^3.0.0" + } + }, + "grunt-known-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", + "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", + "dev": true + }, + "grunt-legacy-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", + "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", + "dev": true, + "requires": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.19" + } + }, + "grunt-legacy-log-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", + "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", + "dev": true, + "requires": { + "chalk": "~4.1.0", + "lodash": "~4.17.19" + } + }, + "grunt-legacy-util": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", + "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", + "dev": true, + "requires": { + "async": "~3.2.0", + "exit": "~0.1.2", + "getobject": "~1.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.21", + "underscore.string": "~3.3.5", + "which": "~2.0.2" + } + }, + "grunt-sass": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-3.1.0.tgz", + "integrity": "sha512-90s27H7FoCDcA8C8+R0GwC+ntYD3lG6S/jqcavWm3bn9RiJTmSfOvfbFa1PXx4NbBWuiGQMLfQTj/JvvqT5w6A==", + "dev": true, + "requires": {} + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "liftup": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", + "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", + "dev": true, + "requires": { + "extend": "^3.0.2", + "findup-sync": "^4.0.0", + "fined": "^1.2.0", + "flagged-respawn": "^1.0.1", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.1", + "rechoir": "^0.7.0", + "resolve": "^1.19.0" + }, + "dependencies": { + "findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + } + } + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "maxmin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-3.0.0.tgz", + "integrity": "sha512-wcahMInmGtg/7c6a75fr21Ch/Ks1Tb+Jtoan5Ft4bAI0ZvJqyOw8kkM7e7p8hDSzY805vmxwHT50KcjGwKyJ0g==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "figures": "^3.2.0", + "gzip-size": "^5.1.1", + "pretty-bytes": "^5.3.0" + } + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "requires": { + "resolve": "^1.9.0" + } + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass": { + "version": "1.49.9", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz", + "integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "requires": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..7943691 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..e0ef91d --- /dev/null +++ b/pom.xml @@ -0,0 +1,359 @@ + + + 4.0.0 + + io.github.mkpaz + atlantafx-parent + pom + 0.1.0 + + AtlantaFX + JavaFX CSS theme collection plus additional controls + https://github.com/mkpaz/atlantafx + + + + MIT License + https://raw.githubusercontent.com/mkpaz/atlantafx/master/LICENSE + repo + + + + + + mkpaz + mkpaz + + + + + scm:git:https://github.com/mkpaz/atlantafx.git + ${project.scm.developerConnection} + ${project.url} + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + base + sampler + styles + + + + UTF-8 + UTF-8 + + ${java.version} + ${java.version} + + 17 + 17.0.0.1 + v16.14.2 + + AtlantaFX + ${project.version} + + 3.12.0 + 11.5.1 + 12.2.0 + 1.3.0 + 22.0.0 + 3.21.0 + 5.8.1 + + + + + + ${project.groupId} + atlantafx-styles + ${project.version} + + + ${project.groupId} + atlantafx-base + ${project.version} + + + + org.openjfx + javafx-controls + ${openjfx.version} + + + org.openjfx + javafx-swing + ${openjfx.version} + + + org.openjfx + javafx-web + ${openjfx.version} + + + org.kordamp.ikonli + ikonli-javafx + ${lib.ikonli.version} + + + org.kordamp.ikonli + ikonli-feather-pack + ${lib.ikonli.version} + + + + fr.brouillard.oss + cssfx + ${lib.cssfx.version} + + + net.datafaker + datafaker + ${lib.datafaker.version} + + + + org.assertj + assertj-core + ${test.assertj.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${test.junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${test.junit.version} + test + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + org.apache.maven.plugins + maven-clean-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0 + + + com.github.eirslett + frontend-maven-plugin + 1.9.1 + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.4.0 + + + org.apache.maven.plugins + maven-install-plugin + 3.0.0-M1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.1.2 + + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-site-plugin + 3.9.1 + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + io.github.wiverson + jtoolprovider-plugin + 1.0.34 + + + org.moditect + moditect-maven-plugin + 1.0.0.RC1 + + + org.openjfx + javafx-maven-plugin + 0.0.6 + + + + + + com.github.eirslett + frontend-maven-plugin + + ${nodejs.version} + ${project.basedir} + + + + install-node-and-npm + + install-node-and-npm + + + + install-npm-packages + generate-resources + + npm + + + install + + + + + + + + + + linux-active + + + unix + + + + linux + + + + windows-active + + + windows + + + + win + + + + sonatype + + false + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-source + compile + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + + + diff --git a/sampler/icons/app-icon.ico b/sampler/icons/app-icon.ico new file mode 100755 index 0000000..a56b1b9 Binary files /dev/null and b/sampler/icons/app-icon.ico differ diff --git a/sampler/icons/app-icon.png b/sampler/icons/app-icon.png new file mode 100755 index 0000000..42610cb Binary files /dev/null and b/sampler/icons/app-icon.png differ diff --git a/sampler/pom.xml b/sampler/pom.xml new file mode 100755 index 0000000..b902dfd --- /dev/null +++ b/sampler/pom.xml @@ -0,0 +1,305 @@ + + + 4.0.0 + + io.github.mkpaz + atlantafx-parent + 0.1.0 + + atlantafx-sampler + + + atlantafx.sampler.Launcher + atlantafx.sampler + + ${app.name}-${app.version}-${platform}-${os.arch} + + ${project.build.directory}${file.separator}dependencies + + ${project.build.directory}${file.separator}platform-modules + + ${project.build.directory}${file.separator}app-image + + ${project.build.directory}${file.separator}app-dir + + ${project.build.directory}${file.separator}runtime-image + + ${project.build.directory}${file.separator}package-scripts + + ${project.build.directory}${file.separator}package-temp + + ${project.build.directory}${file.separator}release + + + + + ${project.groupId} + atlantafx-base + + + org.openjfx + javafx-controls + + + org.openjfx + javafx-swing + + + org.openjfx + javafx-web + + + org.kordamp.ikonli + ikonli-javafx + + + org.kordamp.ikonli + ikonli-feather-pack + + + fr.brouillard.oss + cssfx + + + net.datafaker + datafaker + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + + + org.assertj + assertj-core + + + + + + + + src/main/resources + atlantafx/sampler + false + + + src/main/resources + atlantafx/sampler + true + + application.properties + + + + + src/main/java/atlantafx/sampler/page + atlantafx/sampler/page + false + + **/AbstractPage.java + **/CodeViewer.java + **/Page.java + **/SampleBlock.java + + + + + icons + atlantafx/sampler/assets + false + + + icons + false + ${project.build.directory} + + + + src/package-scripts + true + ${build.package.scriptsDir} + + + + + + org.openjfx + javafx-maven-plugin + + ${java.home}/bin/java + ${app.launcher} + + + + run + + + + + com.github.eirslett + frontend-maven-plugin + + ${project.parent.basedir} + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${build.dependenciesDir} + runtime + org.openjfx + + + + + copy-openjfx + prepare-package + + copy-dependencies + + + ${build.platformModulesDir} + org.openjfx + + + + + + org.apache.maven.plugins + maven-install-plugin + + + true + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${build.dependenciesDir} + + + + io.github.wiverson + jtoolprovider-plugin + + + + create-runtime-image + package + + java-tool + + + jlink + java.base,java.logging,jdk.localedata,java.desktop,javafx.controls,javafx.swing,javafx.web + ${build.platformModulesDir} + ${build.package.runtimeImageDir} + + + --compress=2 + --include-locales=en + --no-header-files + --no-man-pages + --strip-debug + --verbose + + + + + + create-app-image + package + + java-tool + + + jpackage + ${build.package.tempDir} + + @${build.package.scriptsDir}${file.separator}args-base.txt + @${build.package.scriptsDir}${file.separator}args-app-image.txt + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + assembly + install + + single + + + ${build.releaseDir} + ${build.artifactName} + false + false + posix + + src/package-scripts/app-image.xml + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + + + linux-active + + + unix + + + + ${project.build.directory}/app-icon.png + tar.gz + + + + windows-active + + + windows + + + + ${project.build.directory}\app-icon.ico + zip + + + + + diff --git a/sampler/src/main/java/atlantafx/sampler/Launcher.java b/sampler/src/main/java/atlantafx/sampler/Launcher.java new file mode 100755 index 0000000..d6a7089 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/Launcher.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/Resources.java b/sampler/src/main/java/atlantafx/sampler/Resources.java new file mode 100755 index 0000000..29c5df4 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/Resources.java @@ -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)); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/fake/SampleMenuBar.java b/sampler/src/main/java/atlantafx/sampler/fake/SampleMenuBar.java new file mode 100644 index 0000000..4a70453 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/fake/SampleMenuBar.java @@ -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 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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/fake/domain/Book.java b/sampler/src/main/java/atlantafx/sampler/fake/domain/Book.java new file mode 100644 index 0000000..477a785 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/fake/domain/Book.java @@ -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 f) { + Objects.requireNonNull(f); + return f.apply(this); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/fake/domain/Product.java b/sampler/src/main/java/atlantafx/sampler/fake/domain/Product.java new file mode 100644 index 0000000..ac07ff7 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/fake/domain/Product.java @@ -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 f) { + Objects.requireNonNull(f); + return f.apply(this); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java b/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java new file mode 100755 index 0000000..7f6bfef --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java new file mode 100644 index 0000000..d1014b4 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java @@ -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 navigationMenu = navigationMenu(); + private final ReadOnlyObjectWrapper selectedLink = new ReadOnlyObjectWrapper<>(); + private Consumer> 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 pageClass) { + navigationMenu.stream() + .filter(region -> region instanceof NavLink link && pageClass.equals(link.getPageClass())) + .findFirst() + .ifPresent(link -> navigate((NavLink) link)); + } + + public void setOnSelect(Consumer> 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 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 pageClass, String... keywords) { + return navLink(text, pageClass, false, keywords); + } + + @SuppressWarnings("SameParameterValue") + private NavLink navLink(String text, Class 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 pageClass; + private final List searchKeywords = new ArrayList<>(); + + public NavLink(String text, Class 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 getPageClass() { + return pageClass; + } + + public List 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()); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java b/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java new file mode 100755 index 0000000..6ed19d4 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java @@ -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 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 List generate(Supplier supplier, int count) { + return Stream.generate(supplier).limit(count).collect(Collectors.toList()); + } + + protected Feather randomIcon() { + return Feather.values()[RANDOM.nextInt(Feather.values().length)]; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/CodeViewer.java b/sampler/src/main/java/atlantafx/sampler/page/CodeViewer.java new file mode 100644 index 0000000..c9f8015 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/CodeViewer.java @@ -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("") + .append("") + .append("") + .append("") + .append("") + .append("") + // 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("", theme.getBackground())) + .append("
")
+                            .append("")
+                            .append(new String(source.readAllBytes(), UTF_8))
+                            .append("")
+                            .append("
") + .append("") + .append("") + .toString() + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/Page.java b/sampler/src/main/java/atlantafx/sampler/page/Page.java new file mode 100755 index 0000000..e84a1b7 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/Page.java @@ -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(); +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/SampleBlock.java b/sampler/src/main/java/atlantafx/sampler/page/SampleBlock.java new file mode 100644 index 0000000..cb0bf6b --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/SampleBlock.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/AccordionPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/AccordionPage.java new file mode 100644 index 0000000..1329fa9 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/AccordionPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/BreadcrumbsPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/BreadcrumbsPage.java new file mode 100644 index 0000000..0f5784b --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/BreadcrumbsPage.java @@ -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, 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, Button> crumbFactory) { + int count = 5; + TreeItem 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 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 TreeItem getAncestor(TreeItem node, int height) { + var counter = height; + var current = node; + while (counter > 0 && current.getParent() != null) { + current = current.getParent(); + counter--; + } + return current; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ButtonPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ButtonPage.java new file mode 100755 index 0000000..bab1bee --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ButtonPage.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ChartPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ChartPage.java new file mode 100644 index 0000000..3933d28 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ChartPage.java @@ -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 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 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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 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(); + 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(); + 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); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/CheckBoxPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/CheckBoxPage.java new file mode 100644 index 0000000..453aae1 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/CheckBoxPage.java @@ -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() + )); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ColorPickerPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ColorPickerPage.java new file mode 100644 index 0000000..f6fdb9d --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ColorPickerPage.java @@ -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 pickerStyleChoice(ColorPicker colorPicker) { + var optDefault = "Default"; + var optButton = "Button"; + var optSplitButton = "Split Button"; + + var choice = new ChoiceBox(); + 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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ComboBoxPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ComboBoxPage.java new file mode 100644 index 0000000..f1e5b14 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ComboBoxPage.java @@ -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 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 comboBox() { + return comboBox(null); + } + + private ComboBox comboBox(Consumer> mutator) { + var c = new ComboBox(); + c.setPrefWidth(PREF_WIDTH); + if (mutator != null) { mutator.accept(c); } + return c; + } + + private ChoiceBox choiceBox() { + return choiceBox(null); + } + + private ChoiceBox choiceBox(Consumer> mutator) { + var c = new ChoiceBox(); + 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 { + + @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()); + } + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/CustomTextFieldPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/CustomTextFieldPage.java new file mode 100644 index 0000000..b0e739d --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/CustomTextFieldPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/DatePickerPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/DatePickerPage.java new file mode 100644 index 0000000..84af97a --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/DatePickerPage.java @@ -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 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 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); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/DialogPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/DialogPage.java new file mode 100644 index 0000000..dfc3086 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/DialogPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/HTMLEditorPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/HTMLEditorPage.java new file mode 100644 index 0000000..5536105 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/HTMLEditorPage.java @@ -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("

", FAKER.lorem().paragraphs(5))); + + userContent.getChildren().setAll(editor); + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/InputGroupPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/InputGroupPage.java new file mode 100755 index 0000000..c50a899 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/InputGroupPage.java @@ -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); + } +} + diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ListPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ListPage.java new file mode 100644 index 0000000..b35b5d8 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ListPage.java @@ -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 exampleSelect; + + private final List dataList = generate(() -> Book.random(FAKER), 50); + private final StringConverter 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 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 exampleSelect() { + var select = new ComboBox(); + + 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 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> 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> 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 stringList() { + var lv = new ListView(); + lv.getItems().setAll(dataList.stream().map(bookStringConverter::toString).collect(Collectors.toList())); + return lv; + } + + private ListView editableList() { + var lv = new ListView(); + 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 checkBoxList() { + var lv = new ListView(); + lv.setCellFactory(CheckBoxListCell.forListView(Book::stateProperty, bookStringConverter)); + lv.getItems().setAll(dataList.stream().limit(10).collect(Collectors.toList())); + return lv; + } + + private ListView choiceBoxList() { + var lv = new ListView(); + 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 comboBoxList() { + var lv = new ListView(); + 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 nestedControlsList() { + var lv = new ListView(); + 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 { + + 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); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/MenuButtonPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/MenuButtonPage.java new file mode 100755 index 0000000..178e35f --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/MenuButtonPage.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/MenuPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/MenuPage.java new file mode 100644 index 0000000..05d7a39 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/MenuPage.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/OverviewPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/OverviewPage.java new file mode 100755 index 0000000..819cace --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/OverviewPage.java @@ -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(); + comboBox.getItems().setAll("Option 1", "Option 2", "Option 3"); + comboBox.getSelectionModel().selectFirst(); + comboBox.setPrefWidth(COMBO_BOX_WIDTH); + + var choiceBox = new ChoiceBox(); + 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(1, 10, 1); + IntegerStringConverter.createFor(spinner1); + spinner1.setPrefWidth(BUTTON_WIDTH); + + var spinner2 = new Spinner(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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/PaginationPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/PaginationPage.java new file mode 100644 index 0000000..70ae136 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/PaginationPage.java @@ -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(0, 50, 25); + pageCountSpinner.setPrefWidth(PREF_CONTROL_WIDTH); + pagination.pageCountProperty().bind(pageCountSpinner.valueProperty()); + + var visibleCountSpinner = new Spinner(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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/PopoverPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/PopoverPage.java new file mode 100644 index 0000000..306bbb2 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/PopoverPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ProgressPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ProgressPage.java new file mode 100644 index 0000000..28fa6a6 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ProgressPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/RadioButtonPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/RadioButtonPage.java new file mode 100644 index 0000000..868752b --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/RadioButtonPage.java @@ -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)); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ScrollPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ScrollPanePage.java new file mode 100644 index 0000000..9ae18c7 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ScrollPanePage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/SeparatorPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/SeparatorPage.java new file mode 100644 index 0000000..096c842 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/SeparatorPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/SliderPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/SliderPage.java new file mode 100644 index 0000000..984b28e --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/SliderPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/SpinnerPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/SpinnerPage.java new file mode 100644 index 0000000..862087c --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/SpinnerPage.java @@ -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(1, 10, 1); + IntegerStringConverter.createFor(editableSpin); + editableSpin.setPrefWidth(PREF_WIDTH); + editableSpin.setEditable(true); + var editableBlock = new SampleBlock("Editable", editableSpin); + + var disabledSpin = new Spinner(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(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(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(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(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(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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/SplitPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/SplitPanePage.java new file mode 100644 index 0000000..5d7da2e --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/SplitPanePage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TabPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TabPanePage.java new file mode 100644 index 0000000..d922ca9 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TabPanePage.java @@ -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) 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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TablePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TablePage.java new file mode 100644 index 0000000..9dce6f4 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TablePage.java @@ -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 table; + private final List 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 table() { + var stateCol = new TableColumn("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("Index"); + indexCol.setCellFactory(col -> { + TableCell 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("Logo"); + iconCol.setCellValueFactory(c -> new SimpleObjectProperty<>(randomIcon())); + iconCol.setCellFactory(FontIconTableCell.forTableColumn()); + iconCol.setEditable(false); + + var brandCol = new TableColumn("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("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("Price ✎"); + priceCol.setCellValueFactory(new PropertyValueFactory<>("price")); + priceCol.setCellFactory(TextFieldTableCell.forTableColumn()); + priceCol.setEditable(true); + + var stockCountCol = new TableColumn("Count"); + stockCountCol.setCellValueFactory(new PropertyValueFactory<>("count")); + stockCountCol.setEditable(false); + + var stockAvailCol = new TableColumn("Available"); + stockAvailCol.setCellValueFactory(new PropertyValueFactory<>("availability")); + stockAvailCol.setCellFactory(ProgressBarTableCell.forTableColumn()); + stockAvailCol.setEditable(false); + + var stockCol = new TableColumn("Stock"); + stockCol.getColumns().setAll(stockCountCol, stockAvailCol); + + var table = new TableView(); + table.getColumns().setAll(stateCol, indexCol, iconCol, brandCol, nameCol, priceCol, stockCol); + + return table; + } + + private MenuButton settingsMenu(TableView 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) 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 + ); + }}; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TextAreaPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TextAreaPage.java new file mode 100644 index 0000000..05b09d7 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TextAreaPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TextFieldPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TextFieldPage.java new file mode 100644 index 0000000..ce10836 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TextFieldPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TitledPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TitledPanePage.java new file mode 100644 index 0000000..4ed94bc --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TitledPanePage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ToggleButtonPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ToggleButtonPage.java new file mode 100644 index 0000000..149d125 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ToggleButtonPage.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ToggleSwitchPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ToggleSwitchPage.java new file mode 100644 index 0000000..7c76efb --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ToggleSwitchPage.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ToolBarPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ToolBarPage.java new file mode 100644 index 0000000..cf34e87 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ToolBarPage.java @@ -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(); + 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); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TooltipPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TooltipPage.java new file mode 100644 index 0000000..a5683d4 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TooltipPage.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TreePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TreePage.java new file mode 100644 index 0000000..0be72bc --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TreePage.java @@ -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 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 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 exampleSelect() { + var select = new ComboBox(); + 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 newTree = createTree(val); + + // copy existing style classes and properties to the new tree + findDisplayedTree().ifPresent(tv -> { + List 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> findDisplayedTree() { + if (playground == null) { return Optional.empty(); } + return playground.getChildren().stream() + .filter(c -> c instanceof TreeView) + .findFirst() + .map(c -> (TreeView) c); + } + + private TreeView 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 void generateTree(TreeItem parent, Supplier> supplier, int limit, int depth) { + if (limit == 0) { return; } + + var item = supplier.get(); + parent.getChildren().add(item); + + TreeItem 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 stringTree() { + var root = new TreeItem<>("root"); + root.setExpanded(true); + + var tree = new TreeView(); + + generateTree(root, () -> new TreeItem<>(FAKER.internet().domainWord()), 30, 1); + tree.setRoot(root); + + return tree; + } + + private TreeView graphicTree() { + var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER)); + root.setExpanded(true); + + var tree = new TreeView(); + + generateTree(root, () -> new TreeItem<>(FAKER.internet().domainWord(), new FontIcon(Feather.FILE)), 30, 1); + tree.setRoot(root); + + return tree; + } + + private TreeView editableTree() { + var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER)); + root.setExpanded(true); + + var tree = new TreeView(); + 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 checkBoxTree() { + var root = new CheckBoxTreeItem<>("root"); + root.setExpanded(true); + + var tree = new TreeView(); + tree.setCellFactory(CheckBoxTreeCell.forTreeView()); + + generateTree(root, () -> new CheckBoxTreeItem<>(FAKER.internet().domainWord()), 30, 1); + tree.setRoot(root); + + return tree; + } + + private TreeView choiceBoxTree() { + var root = new TreeItem<>("root"); + root.setExpanded(true); + + var tree = new TreeView(); + 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 comboBoxTree() { + var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER)); + root.setExpanded(true); + + var tree = new TreeView(); + 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); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TreeTablePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TreeTablePage.java new file mode 100644 index 0000000..6eaeb02 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TreeTablePage.java @@ -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 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> 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 treeTable() { + var arrowCol = new TreeTableColumn("#"); + // 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("Selected"); + stateCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("state")); + stateCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(stateCol)); + stateCol.setEditable(true); + + var idCol = new TreeTableColumn("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("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("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("Price ✎"); + priceCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("price")); + priceCol.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn()); + priceCol.setEditable(true); + + var table = new TreeTableView(); + table.getColumns().setAll(arrowCol, stateCol, brandCol, idCol, nameCol, priceCol); + + return table; + } + + private MenuButton settingsMenu(TreeTableView 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) 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 + ); + }}; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java new file mode 100755 index 0000000..15b995e --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java @@ -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 themeSelector = themeSelector(); + themeSelector.setPrefWidth(200); + + Spinner 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 themeSelector() { + var manager = ThemeManager.getInstance(); + var selector = new ChoiceBox(); + 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 fontSizeSpinner() { + var spinner = new Spinner(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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java b/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java new file mode 100755 index 0000000..358f1ed --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java @@ -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()) + )); + } + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/Chat.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/Chat.java new file mode 100644 index 0000000..27a8bb1 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/Chat.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/FileManager.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/FileManager.java new file mode 100644 index 0000000..cd50dff --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/FileManager.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/MusicPlayer.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/MusicPlayer.java new file mode 100644 index 0000000..bb510b8 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/MusicPlayer.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/TextEditor.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/TextEditor.java new file mode 100644 index 0000000..90a1e48 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/TextEditor.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/theme/ExternalTheme.java b/sampler/src/main/java/atlantafx/sampler/theme/ExternalTheme.java new file mode 100644 index 0000000..9e209aa --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/theme/ExternalTheme.java @@ -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 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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/theme/HighlightJSTheme.java b/sampler/src/main/java/atlantafx/sampler/theme/HighlightJSTheme.java new file mode 100644 index 0000000..f40d637 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/theme/HighlightJSTheme.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java b/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java new file mode 100644 index 0000000..61429cd --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java @@ -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 getAvailableThemes() { + var themes = new ArrayList(); + 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 Set merge(T first, T... arr) { + var set = new LinkedHashSet(); + 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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/util/Containers.java b/sampler/src/main/java/atlantafx/sampler/util/Containers.java new file mode 100755 index 0000000..9de631c --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/util/Containers.java @@ -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; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/util/Controls.java b/sampler/src/main/java/atlantafx/sampler/util/Controls.java new file mode 100644 index 0000000..43d89a0 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/util/Controls.java @@ -0,0 +1,55 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.util; + +import javafx.scene.control.Button; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.KeyCombination; +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.javafx.FontIcon; + +import static atlantafx.base.theme.Styles.BUTTON_ICON; + +public final class Controls { + + public static Button iconButton(Ikon icon, boolean disable) { + return button("", icon, disable, BUTTON_ICON); + } + + public static Button button(String text, Ikon icon, boolean disable, String... styleClasses) { + var button = new Button(text); + if (icon != null) { button.setGraphic(new FontIcon(icon)); } + button.setDisable(disable); + button.getStyleClass().addAll(styleClasses); + return button; + } + + public static MenuItem menuItem(String text, Ikon graphic, KeyCombination accelerator) { + return menuItem(text, graphic, accelerator, false); + } + + public static MenuItem menuItem(String text, Ikon graphic, KeyCombination accelerator, boolean disable) { + var item = new MenuItem(text); + + if (graphic != null) { item.setGraphic(new FontIcon(graphic)); } + if (accelerator != null) { item.setAccelerator(accelerator); } + item.setDisable(disable); + + return item; + } + + public static ToggleButton toggleButton(String text, + Ikon icon, + ToggleGroup group, + boolean selected, + String... styleClasses) { + var toggleButton = new ToggleButton(text); + if (icon != null) { toggleButton.setGraphic(new FontIcon(icon)); } + if (group != null) { toggleButton.setToggleGroup(group); } + toggleButton.setSelected(selected); + toggleButton.getStyleClass().addAll(styleClasses); + + return toggleButton; + } +} diff --git a/sampler/src/main/java/module-info.java b/sampler/src/main/java/module-info.java new file mode 100755 index 0000000..f32f9a4 --- /dev/null +++ b/sampler/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: MIT */ + +module atlantafx.sampler { + + requires atlantafx.base; + + requires java.desktop; + requires javafx.swing; + requires javafx.web; + + requires org.kordamp.ikonli.core; + requires org.kordamp.ikonli.javafx; + requires org.kordamp.ikonli.feather; + + requires fr.brouillard.oss.cssfx; + requires datafaker; + + exports atlantafx.sampler; + exports atlantafx.sampler.fake; + exports atlantafx.sampler.fake.domain; + exports atlantafx.sampler.layout; + exports atlantafx.sampler.page; + exports atlantafx.sampler.page.general; + exports atlantafx.sampler.page.components; + exports atlantafx.sampler.page.showcase; + exports atlantafx.sampler.theme; + exports atlantafx.sampler.util; + + opens atlantafx.sampler.fake.domain; + + // resources + opens atlantafx.sampler.assets.fonts; + opens atlantafx.sampler.assets.highlightjs; + opens atlantafx.sampler.assets.styles; +} diff --git a/sampler/src/main/resources/application.properties b/sampler/src/main/resources/application.properties new file mode 100755 index 0000000..8a0318d --- /dev/null +++ b/sampler/src/main/resources/application.properties @@ -0,0 +1,4 @@ +app.name=AtlantaFX Sampler +app.description=${project.description} +app.homepage=${project.url} +app.version=${project.version} diff --git a/sampler/src/main/resources/assets/fonts/Inter/InterUI-Bold.otf b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Bold.otf new file mode 100755 index 0000000..d5c49b8 Binary files /dev/null and b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Bold.otf differ diff --git a/sampler/src/main/resources/assets/fonts/Inter/InterUI-Italic.otf b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Italic.otf new file mode 100755 index 0000000..eb772c8 Binary files /dev/null and b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Italic.otf differ diff --git a/sampler/src/main/resources/assets/fonts/Inter/InterUI-Medium.otf b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Medium.otf new file mode 100755 index 0000000..c5075ca Binary files /dev/null and b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Medium.otf differ diff --git a/sampler/src/main/resources/assets/fonts/Inter/InterUI-Regular.otf b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Regular.otf new file mode 100755 index 0000000..f3a05e7 Binary files /dev/null and b/sampler/src/main/resources/assets/fonts/Inter/InterUI-Regular.otf differ diff --git a/sampler/src/main/resources/assets/fonts/index.css b/sampler/src/main/resources/assets/fonts/index.css new file mode 100755 index 0000000..5139784 --- /dev/null +++ b/sampler/src/main/resources/assets/fonts/index.css @@ -0,0 +1,30 @@ +/* + * Note that each font-family have to be unique. That's because unlike web browsers + * OpenJFX CSS parser doesn't support `font-weight` and `font-style` attributes + * in the `@font-face` at-rule. It just silently ignores them. So, the font variant + * can only be expressed via font name. OpenJFX always uses font transformation, + * even when corresponding font variant provided explicitly. + * + * See, CSS Reference guide: + * https://openjfx.io/javadoc/17/javafx.graphics/javafx/scene/doc-files/cssref.html + * > Although the parser will parse the syntax, all @font‑face descriptors + * > are ignored except for the src descriptor. The src descriptor is expected + * > to be a . The format hint is ignored. + */ + +@font-face { + font-family: "Inter UI Regular"; + src: url('Inter/InterUI-Regular.otf'); +} +@font-face { + font-family: "Inter UI Medium"; + src: url('Inter/InterUI-Medium.otf'); +} +@font-face { + font-family: "Inter UI Bold"; + src: url('Inter/InterUI-Bold.otf'); +} +@font-face { + font-family: "Inter UI Italic"; + src: url('Inter/InterUI-Italic.otf'); +} diff --git a/sampler/src/main/resources/assets/highlightjs/highlight.min.js b/sampler/src/main/resources/assets/highlightjs/highlight.min.js new file mode 100644 index 0000000..d2220d1 --- /dev/null +++ b/sampler/src/main/resources/assets/highlightjs/highlight.min.js @@ -0,0 +1,340 @@ +/*! + Highlight.js v11.5.1 (git: b8f233c8e2) + (c) 2006-2022 Ivan Sagalaev and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";var e={exports:{}};function t(e){ +return e instanceof Map?e.clear=e.delete=e.set=()=>{ +throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n] +;"object"!=typeof i||Object.isFrozen(i)||t(i)})),e} +e.exports=t,e.exports.default=t;var n=e.exports;class i{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function r(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function s(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const o=e=>!!e.kind +;class a{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=r(e)}openNode(e){if(!o(e))return;let t=e.kind +;t=e.sublanguage?"language-"+t:((e,{prefix:t})=>{if(e.includes(".")){ +const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(t,{prefix:this.classPrefix}),this.span(t)}closeNode(e){ +o(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}class c{constructor(){this.rootNode={ +children:[]},this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t={kind:e,children:[]} +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +c._collapse(e)})))}}class l extends c{constructor(e){super(),this.options=e} +addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())} +addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root +;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){ +return new a(this,this.options).value()}finalize(){return!0}}function g(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return f("(?=",e,")")} +function u(e){return f("(?:",e,")*")}function h(e){return f("(?:",e,")?")} +function f(...e){return e.map((e=>g(e))).join("")}function p(...e){const t=(e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e);return"("+(t.capture?"":"?:")+e.map((e=>g(e))).join("|")+")"} +function b(e){return RegExp(e.toString()+"|").exec("").length-1} +const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function E(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=g(e),r="";for(;i.length>0;){const e=m.exec(i);if(!e){r+=i;break} +r+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0], +"("===e[0]&&n++)}return r})).map((e=>`(${e})`)).join(t)} +const x="[a-zA-Z]\\w*",w="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",k="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},N={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},M=(e,t,n={})=>{const i=s({scope:"comment",begin:e,end:t, +contains:[]},n);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const r=p("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:f(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},S=M("//","$"),R=M("/\\*","\\*/"),j=M("#","$");var A=Object.freeze({ +__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:x,UNDERSCORE_IDENT_RE:w, +NUMBER_RE:y,C_NUMBER_RE:_,BINARY_NUMBER_RE:k, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=f(t,/.*\b/,e.binary,/\b.*/)),s({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +BACKSLASH_ESCAPE:v,APOS_STRING_MODE:O,QUOTE_STRING_MODE:N,PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},COMMENT:M,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:R,HASH_COMMENT_MODE:j, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},C_NUMBER_MODE:{scope:"number", +begin:_,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:k,relevance:0}, +REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//, +end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0, +contains:[v]}]}]},TITLE_MODE:{scope:"title",begin:x,relevance:0}, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:w,relevance:0},METHOD_GUARD:{ +begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function I(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function T(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function L(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=I,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function B(e,t){ +Array.isArray(e.illegal)&&(e.illegal=p(...e.illegal))}function D(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function H(e,t){ +void 0===e.relevance&&(e.relevance=1)}const P=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords,e.begin=f(n.beforeMatch,d(n.begin)),e.starts={ +relevance:0,contains:[Object.assign(n,{endsParent:!0})] +},e.relevance=0,delete n.beforeMatch +},C=["of","and","for","in","not","or","if","then","parent","list","value"] +;function $(e,t,n="keyword"){const i=Object.create(null) +;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,$(e[n],t,n))})),i;function r(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ +return t?Number(t):(e=>C.includes(e.toLowerCase()))(e)?0:1}const z={},K=e=>{ +console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{ +z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) +},G=Error();function Z(e,t,{key:n}){let i=0;const r=e[n],s={},o={} +;for(let e=1;e<=t.length;e++)o[e+i]=r[e],s[e+i]=!0,i+=b(t[e-1]) +;e[n]=o,e[n]._emit=s,e[n]._multi=!0}function F(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=E(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=E(e.end,{joinWith:""})}})(e)}function V(e){ +function t(t,n){ +return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) +}class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=b(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(E(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=s(e.classNameAliases||{}),function n(r,o){const a=r +;if(r.isCompiled)return a +;[T,D,F,P].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))), +r.__beforeBegin=null,[L,B,H].forEach((e=>e(r,o))),r.isCompiled=!0;let c=null +;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), +c=r.keywords.$pattern, +delete r.keywords.$pattern),c=c||/\w+/,r.keywords&&(r.keywords=$(r.keywords,e.case_insensitive)), +a.keywordPatternRe=t(c,!0), +o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(a.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), +r.end&&(a.endRe=t(a.end)), +a.terminatorEnd=g(a.end)||"",r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+o.terminatorEnd)), +r.illegal&&(a.illegalRe=t(r.illegal)), +r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>s(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?s(e,{ +starts:e.starts?s(e.starts):null +}):Object.isFrozen(e)?s(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{n(e,a) +})),r.starts&&n(r.starts,o),a.matcher=(e=>{const t=new i +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ +return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ +constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} +const Y=r,Q=s,ee=Symbol("nomatch");var te=(e=>{ +const t=Object.create(null),r=Object.create(null),s=[];let o=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let g={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function b(e){ +return g.noHighlightRe.test(e)}function m(e,t,n){let i="",r="" +;"object"==typeof t?(i=e, +n=t.ignoreIllegals,r=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."), +X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +r=e,i=t),void 0===n&&(n=!0);const s={code:i,language:r};N("before:highlight",s) +;const o=s.result?s.result:E(s.language,s.code,n) +;return o.code=s.code,N("after:highlight",o),o}function E(e,n,r,s){ +const c=Object.create(null);function l(){if(!O.keywords)return void M.addText(S) +;let e=0;O.keywordPatternRe.lastIndex=0;let t=O.keywordPatternRe.exec(S),n="" +;for(;t;){n+=S.substring(e,t.index) +;const r=y.case_insensitive?t[0].toLowerCase():t[0],s=(i=r,O.keywords[i]);if(s){ +const[e,i]=s +;if(M.addText(n),n="",c[r]=(c[r]||0)+1,c[r]<=7&&(R+=i),e.startsWith("_"))n+=t[0];else{ +const n=y.classNameAliases[e]||e;M.addKeyword(t[0],n)}}else n+=t[0] +;e=O.keywordPatternRe.lastIndex,t=O.keywordPatternRe.exec(S)}var i +;n+=S.substr(e),M.addText(n)}function d(){null!=O.subLanguage?(()=>{ +if(""===S)return;let e=null;if("string"==typeof O.subLanguage){ +if(!t[O.subLanguage])return void M.addText(S) +;e=E(O.subLanguage,S,!0,N[O.subLanguage]),N[O.subLanguage]=e._top +}else e=x(S,O.subLanguage.length?O.subLanguage:null) +;O.relevance>0&&(R+=e.relevance),M.addSublanguage(e._emitter,e.language) +})():l(),S=""}function u(e,t){let n=1;const i=t.length-1;for(;n<=i;){ +if(!e._emit[n]){n++;continue}const i=y.classNameAliases[e[n]]||e[n],r=t[n] +;i?M.addKeyword(r,i):(S=r,l(),S=""),n++}}function h(e,t){ +return e.scope&&"string"==typeof e.scope&&M.openNode(y.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(M.addKeyword(S,y.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +S=""):e.beginScope._multi&&(u(e.beginScope,t),S="")),O=Object.create(e,{parent:{ +value:O}}),O}function f(e,t,n){let r=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,n);if(r){if(e["on:end"]){const n=new i(e) +;e["on:end"](t,n),n.isMatchIgnored&&(r=!1)}if(r){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return f(e.parent,t,n)}function p(e){ +return 0===O.matcher.regexIndex?(S+=e[0],1):(I=!0,0)}function b(e){ +const t=e[0],i=n.substr(e.index),r=f(O,e,i);if(!r)return ee;const s=O +;O.endScope&&O.endScope._wrap?(d(), +M.addKeyword(t,O.endScope._wrap)):O.endScope&&O.endScope._multi?(d(), +u(O.endScope,e)):s.skip?S+=t:(s.returnEnd||s.excludeEnd||(S+=t), +d(),s.excludeEnd&&(S=t));do{ +O.scope&&M.closeNode(),O.skip||O.subLanguage||(R+=O.relevance),O=O.parent +}while(O!==r.parent);return r.starts&&h(r.starts,e),s.returnEnd?0:t.length} +let m={};function w(t,s){const a=s&&s[0];if(S+=t,null==a)return d(),0 +;if("begin"===m.type&&"end"===s.type&&m.index===s.index&&""===a){ +if(S+=n.slice(s.index,s.index+1),!o){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=m.rule,t}return 1} +if(m=s,"begin"===s.type)return(e=>{ +const t=e[0],n=e.rule,r=new i(n),s=[n.__beforeBegin,n["on:begin"]] +;for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return p(t) +;return n.skip?S+=t:(n.excludeBegin&&(S+=t), +d(),n.returnBegin||n.excludeBegin||(S=t)),h(n,e),n.returnBegin?0:t.length})(s) +;if("illegal"===s.type&&!r){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(O.scope||"")+'"') +;throw e.mode=O,e}if("end"===s.type){const e=b(s);if(e!==ee)return e} +if("illegal"===s.type&&""===a)return 1 +;if(A>1e5&&A>3*s.index)throw Error("potential infinite loop, way more iterations than matches") +;return S+=a,a.length}const y=k(e) +;if(!y)throw K(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const _=V(y);let v="",O=s||_;const N={},M=new g.__emitter(g);(()=>{const e=[] +;for(let t=O;t!==y;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>M.openNode(e)))})();let S="",R=0,j=0,A=0,I=!1;try{ +for(O.matcher.considerAll();;){ +A++,I?I=!1:O.matcher.considerAll(),O.matcher.lastIndex=j +;const e=O.matcher.exec(n);if(!e)break;const t=w(n.substring(j,e.index),e) +;j=e.index+t}return w(n.substr(j)),M.closeAllNodes(),M.finalize(),v=M.toHTML(),{ +language:e,value:v,relevance:R,illegal:!1,_emitter:M,_top:O}}catch(t){ +if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n), +illegal:!0,relevance:0,_illegalBy:{message:t.message,index:j, +context:n.slice(j-100,j+100),mode:t.mode,resultSoFar:v},_emitter:M};if(o)return{ +language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:O} +;throw t}}function x(e,n){n=n||g.languages||Object.keys(t);const i=(e=>{ +const t={value:Y(e),illegal:!1,relevance:0,_top:c,_emitter:new g.__emitter(g)} +;return t._emitter.addText(e),t})(e),r=n.filter(k).filter(O).map((t=>E(t,e,!1))) +;r.unshift(i);const s=r.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(k(e.language).supersetOf===t.language)return 1 +;if(k(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=s,l=o +;return l.secondBest=a,l}function w(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=g.languageDetectRe.exec(t);if(n){const t=k(n[1]) +;return t||(W(a.replace("{}",n[1])), +W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>b(e)||k(e)))})(e);if(b(n))return +;if(N("before:highlightElement",{el:e,language:n +}),e.children.length>0&&(g.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),g.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) +;t=e;const i=t.textContent,s=n?m(i,{language:n,ignoreIllegals:!0}):x(i) +;e.innerHTML=s.value,((e,t,n)=>{const i=t&&r[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,s.language),e.result={language:s.language,re:s.relevance, +relevance:s.relevance},s.secondBest&&(e.secondBest={ +language:s.secondBest.language,relevance:s.secondBest.relevance +}),N("after:highlightElement",{el:e,result:s,text:i})}let y=!1;function _(){ +"loading"!==document.readyState?document.querySelectorAll(g.cssSelector).forEach(w):y=!0 +}function k(e){return e=(e||"").toLowerCase(),t[e]||t[r[e]]} +function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +r[e.toLowerCase()]=t}))}function O(e){const t=k(e) +;return t&&!t.disableAutodetect}function N(e,t){const n=e;s.forEach((e=>{ +e[n]&&e[n](t)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +y&&_()}),!1),Object.assign(e,{highlight:m,highlightAuto:x,highlightAll:_, +highlightElement:w, +highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"), +X("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{g=Q(g,e)}, +initHighlighting:()=>{ +_(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +_(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(n,i)=>{let r=null;try{r=i(e)}catch(e){ +if(K("Language definition for '{}' could not be registered.".replace("{}",n)), +!o)throw e;K(e),r=c} +r.name||(r.name=n),t[n]=r,r.rawDefinition=i.bind(null,e),r.aliases&&v(r.aliases,{ +languageName:n})},unregisterLanguage:e=>{delete t[e] +;for(const t of Object.keys(r))r[t]===e&&delete r[t]}, +listLanguages:()=>Object.keys(t),getLanguage:k,registerAliases:v, +autoDetection:O,inherit:Q,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)} +}),e.debugMode=()=>{o=!1},e.safeMode=()=>{o=!0 +},e.versionString="11.5.1",e.regex={concat:f,lookahead:d,either:p,optional:h, +anyNumberOfTimes:u};for(const e in A)"object"==typeof A[e]&&n(A[e]) +;return Object.assign(e,A),e})({});return te}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `java` grammar compiled for Highlight.js 11.5.1 */ +(()=>{var e=(()=>{"use strict" +;var e="\\.([0-9](_*[0-9])*)",a="[0-9a-fA-F](_*[0-9a-fA-F])*",n={ +className:"number",variants:[{ +begin:`(\\b([0-9](_*[0-9])*)((${e})|\\.)?|(${e}))[eE][+-]?([0-9](_*[0-9])*)[fFdD]?\\b` +},{begin:`\\b([0-9](_*[0-9])*)((${e})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ +begin:`(${e})[fFdD]?\\b`},{begin:"\\b([0-9](_*[0-9])*)[fFdD]\\b"},{ +begin:`\\b0[xX]((${a})\\.?|(${a})?\\.(${a}))[pP][+-]?([0-9](_*[0-9])*)[fFdD]?\\b` +},{begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${a})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};function s(e,a,n){return-1===n?"":e.replace(a,(t=>s(e,a,n-1)))} +return e=>{ +const a=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",i=t+s("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),r={ +keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed"], +literal:["false","true","null"], +type:["char","boolean","long","float","int","byte","short","double"], +built_in:["super","this"]},l={className:"meta",begin:"@"+t,contains:[{ +begin:/\(/,end:/\)/,contains:["self"]}]},c={className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} +;return{name:"Java",aliases:["jsp"],keywords:r,illegal:/<\/|#/, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, +relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ +begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, +className:"string",contains:[e.BACKSLASH_ESCAPE] +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ +match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ +1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ +begin:[a.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=/],className:{1:"type", +3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", +3:"title.class"},contains:[c,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"new throw return else",relevance:0},{ +begin:["(?:"+i+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ +2:"title.function"},keywords:r,contains:[{className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0, +contains:[l,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,n,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},n,l]}}})() +;hljs.registerLanguage("java",e)})(); \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/empty.css b/sampler/src/main/resources/assets/styles/empty.css new file mode 100644 index 0000000..4d8db7d --- /dev/null +++ b/sampler/src/main/resources/assets/styles/empty.css @@ -0,0 +1 @@ +/* This is dummy file to clear user agent stylesheet. */ diff --git a/sampler/src/main/resources/assets/styles/index.css b/sampler/src/main/resources/assets/styles/index.css new file mode 100755 index 0000000..8574e3c --- /dev/null +++ b/sampler/src/main/resources/assets/styles/index.css @@ -0,0 +1,84 @@ +/** SPDX-License-Identifier: MIT */ + +.root { + -fx-font-family: "Inter"; +} + +#sidebar { + -fx-padding: 0 0 12px 0; + -fx-background-color: -color-bg-inset; + -fx-border-color: -color-border-default; + -fx-border-width: 0 1px 0 0; +} +#sidebar #search-form { + -fx-padding: 12px; + -fx-border-color: -color-border-muted; + -fx-border-width: 0 0 1px 0; +} +#sidebar .nav-menu { + -fx-padding: 0 6px 0 6px; +} +#sidebar .nav-menu > .caption { + -fx-padding: 10px 0 10px 6px; + -fx-font-weight: bold; + -fx-text-fill: -color-fg-muted; +} +#sidebar .nav-menu > .nav-link { + -fx-padding: 6px 12px 6px 12px; +} +#sidebar .nav-menu > .nav-link:hover { + -fx-background-color: -color-accent-muted; + -fx-background-radius: 6px; +} +#sidebar .nav-menu > .nav-link:selected { + -fx-text-fill: -color-accent-fg; + -fx-font-weight: bold; +} +#sidebar .nav-menu > .nav-link > .tag { + -fx-background-color: -color-accent-emphasis; + -fx-text-fill: -color-fg-emphasis; + -fx-padding: 2px; + -fx-background-radius: 4px; +} + +.page > .header { + -fx-padding: 10px 20px 14px 20px; + -fx-spacing: 10px; +} +.page > .stack > .scroll-pane { + -fx-background-color: -color-bg-default; +} +/* wrapper is used to center the content and also guarantees some minimum paddings via min-width */ +.page > .stack > .scroll-pane > .viewport > * > .wrapper { + -fx-min-width: 880px; + -fx-alignment: TOP_CENTER; +} +.page > .stack > .scroll-pane > .viewport > * > .wrapper > .user-content { + -fx-padding: 40px 0 40px 0; + -fx-spacing: 40px; + -fx-max-width: 800px; +} +.page > .stack > .wrapper { + -fx-background-color: -color-bg-default; + -fx-alignment: TOP_CENTER; +} +.page > .stack > .wrapper > .code-viewer { + -fx-background-color: transparent; + -fx-padding: 0px 0px 20px 20px; + -fx-max-width: 1000px; +} +.page > .stack > .wrapper > .code-viewer > .web-view { + -fx-background-color: transparent; +} + +.sample-block { + -fx-spacing: 1em; +} +.sample-block>.title { + -fx-font-weight: bold; +} + +.bordered { + -fx-border-width: 1px; + -fx-border-color: -color-border-muted; +} diff --git a/sampler/src/main/resources/images/20_min_adventure.jpg b/sampler/src/main/resources/images/20_min_adventure.jpg new file mode 100644 index 0000000..cce3a63 Binary files /dev/null and b/sampler/src/main/resources/images/20_min_adventure.jpg differ diff --git a/sampler/src/package-scripts/app-image.xml b/sampler/src/package-scripts/app-image.xml new file mode 100755 index 0000000..02ee1ac --- /dev/null +++ b/sampler/src/package-scripts/app-image.xml @@ -0,0 +1,20 @@ + + + + assembly + + ${app.build.compressionAlg} + + false + + + + ${build.package.appImageDir}/${app.name} + / + false + + + + \ No newline at end of file diff --git a/sampler/src/package-scripts/args-app-image.txt b/sampler/src/package-scripts/args-app-image.txt new file mode 100644 index 0000000..7f0be24 --- /dev/null +++ b/sampler/src/package-scripts/args-app-image.txt @@ -0,0 +1,5 @@ +--type app-image +--module "${app.module}/${app.launcher}" +--module-path "${build.dependenciesDir}" +--runtime-image "${build.package.runtimeImageDir}" +--dest "${build.package.appImageDir}" diff --git a/sampler/src/package-scripts/args-base.txt b/sampler/src/package-scripts/args-base.txt new file mode 100755 index 0000000..96576ca --- /dev/null +++ b/sampler/src/package-scripts/args-base.txt @@ -0,0 +1,5 @@ +--name "${app.name}" +--icon "${app.icon}" +--app-version "${app.version}" +--temp "${build.package.tempDir}" +--verbose \ No newline at end of file diff --git a/sampler/src/package-scripts/build.properties b/sampler/src/package-scripts/build.properties new file mode 100755 index 0000000..01db3fc --- /dev/null +++ b/sampler/src/package-scripts/build.properties @@ -0,0 +1 @@ +artifactName=${build.artifactName} diff --git a/styles/Gruntfile.js b/styles/Gruntfile.js new file mode 100755 index 0000000..7b8d158 --- /dev/null +++ b/styles/Gruntfile.js @@ -0,0 +1,29 @@ +module.exports = function (grunt) { + const SASS_COMPILER = require('sass'); + const CSS_OUTPUT_DIR = process.env.NODE_ENV !== 'dev' ? 'dist' : '../sampler/target/classes/atlantafx/sampler/theme-test'; + + grunt.initConfig({ + cssOutputDir: CSS_OUTPUT_DIR, + sass: { + + options: { + implementation: SASS_COMPILER + }, + dist: { + files: { + '<%= cssOutputDir %>/primer-light.css': [ 'src/primer-light.scss' ], + '<%= cssOutputDir %>/primer-dark.css': [ 'src/primer-dark.scss' ] + } + } + }, + cssmin: { + dist: { + files: {} + } + } + }); + + grunt.loadNpmTasks('grunt-sass'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + grunt.registerTask('default', ['sass', 'cssmin']); +}; diff --git a/styles/assembly.xml b/styles/assembly.xml new file mode 100755 index 0000000..b81ec59 --- /dev/null +++ b/styles/assembly.xml @@ -0,0 +1,20 @@ + + + + assembly + + zip + + false + + + + dist/ + / + false + + + + \ No newline at end of file diff --git a/styles/compile-scss-dev.sh b/styles/compile-scss-dev.sh new file mode 100755 index 0000000..3cb130d --- /dev/null +++ b/styles/compile-scss-dev.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +# This scipt compiles SCSS directly to the sampler classpath for hot reload (CSSFX). + +PATH="../node:$(../node/npm bin):$PATH" +NODE_ENV=dev grunt --verbose --gruntfile=Gruntfile.js diff --git a/styles/pom.xml b/styles/pom.xml new file mode 100755 index 0000000..eb94f16 --- /dev/null +++ b/styles/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + io.github.mkpaz + atlantafx-parent + 0.1.0 + + atlantafx-styles + + + + + + com.github.eirslett + frontend-maven-plugin + + ${project.parent.basedir} + + + + run-grunt + generate-resources + + grunt + + + --verbose + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + assembly + install + + single + + + ${project.build.directory} + ${app.name}-${app.version}-themes + false + false + posix + + assembly.xml + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + diff --git a/styles/src/components/_accordion.scss b/styles/src/components/_accordion.scss new file mode 100755 index 0000000..0287bd6 --- /dev/null +++ b/styles/src/components/_accordion.scss @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +.accordion { + + // make the rule more specific, otherwise it'd need to + // be placed below '>.titled-pane' rules + >.titled-pane.first-titled-pane { + >.title { + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: cfg.$border-radius cfg.$border-radius 0 0; + } + } + + >.titled-pane { + >.title { + -fx-background-insets: 0, 0 cfg.$border-width cfg.$border-width cfg.$border-width; + // there's no such class as 'last-titled-pane, so we can't add + // border radius to the last accordion block whick looks a bit ugly + -fx-background-radius: 0; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_breadcrumbs.scss b/styles/src/components/_breadcrumbs.scss new file mode 100755 index 0000000..1dc44de --- /dev/null +++ b/styles/src/components/_breadcrumbs.scss @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +// .bread-crumb-bar { +// >.crumb { +// ... +// } +// } \ No newline at end of file diff --git a/styles/src/components/_button.scss b/styles/src/components/_button.scss new file mode 100755 index 0000000..5ebe26e --- /dev/null +++ b/styles/src/components/_button.scss @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/icons"; +@use "../settings/utils"; + +// basic style is shared with Button, ToggleButton and MenuButton +@mixin base() { + + // buttons can be configured via looked-up colors + -color-button-bg: -color-bg-inset; + -color-button-fg: -color-fg-default; + -color-button-border: -color-border-default; + + -color-button-bg-hover: utils.saturate(-color-button-bg, cfg.$darkMode, cfg.$color-delta-hover); + -color-button-fg-hover: -color-button-fg; + -color-button-border-hover: -color-button-border; + + -color-button-bg-focused: -color-button-bg; + -color-button-fg-focused: -color-button-fg; + -color-button-border-focused: -color-accent-emphasis; + + -color-button-bg-pressed: utils.saturate(-color-button-bg, cfg.$darkMode, cfg.$color-delta-active); + -color-button-fg-pressed: -color-button-fg; + -color-button-border-pressed: -color-button-border; + + -fx-background-color: -color-button-border, -color-button-bg; + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: cfg.$border-radius; + -fx-graphic-text-gap: cfg.$graphic-gap; + -fx-text-fill: -color-button-fg; + -fx-alignment: CENTER; + + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-button-fg; + // Always add -fx-fill when styling font icons. There's at least one weird bug. + // If you set stylesheet to the scene without '-fx-fill' font icon fg will be + // applied correctly. But if you set exactly the same stylesheet via + // Application.setUserAgentStylesheet(), font icon fg won't be correct without + // using '-fx-fill' here. + -fx-fill: -color-button-fg; + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + + &:show-mnemonics { + >.mnemonic-underline { + -fx-stroke: -color-button-fg; + } + } + + // Button calculates its height based on the internal text font size. + // So, button that has no text (or icon) will appear lower than regulat + // buttons. There're no workarounds, but you can set escape char like + // \s or \f as button text. + &.button-icon { + -fx-padding: cfg.$padding-y; + + >.text { + visibility: hidden; + } + } + + &.button-circle { + -fx-background-radius: 50; + -fx-padding: 6px 8px 6px 8px; + + .text { + visibility: hidden; + } + } + + // toggle button + &.left-pill { + -fx-background-radius: cfg.$border-radius 0 0 cfg.$border-radius; + -fx-background-insets: 0, cfg.$border-width 0 cfg.$border-width cfg.$border-width; + + &:hover, + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } + + &.center-pill { + -fx-background-radius: 0; + -fx-background-insets: 0, cfg.$border-width 0 cfg.$border-width 0; + + &:hover, + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } + + &.right-pill { + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-background-insets: 0, cfg.$border-width cfg.$border-width cfg.$border-width 0; + + &:hover, + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } +} + +@mixin accent() { + -color-button-bg: -color-accent-emphasis; + -color-button-fg: -color-fg-emphasis; + -color-button-border: -color-accent-emphasis; + -color-button-border-focused: -color-accent-emphasis; + + &.button-outlined { + -color-button-bg: -color-bg-default; + -color-button-fg: -color-accent-fg; + + -color-button-bg-hover: -color-accent-emphasis; + -color-button-fg-hover: -color-fg-emphasis; + + -color-button-bg-focused: -color-accent-emphasis; + -color-button-fg-focused: -color-fg-emphasis; + + -color-button-bg-pressed: utils.saturate(-color-accent-emphasis, cfg.$darkMode, cfg.$color-delta-active); + -color-button-fg-pressed: -color-fg-emphasis; + } + + &.flat { + -color-button-fg: -color-accent-emphasis; + -color-button-bg-hover: -color-accent-subtle; + } +} + +@mixin success() { + -color-button-bg: -color-success-emphasis; + -color-button-fg: -color-fg-emphasis; + -color-button-border: -color-success-emphasis; + -color-button-border-focused: -color-success-emphasis; + + &.button-outlined { + -color-button-bg: -color-bg-default; + -color-button-fg: -color-success-fg; + + -color-button-bg-hover: -color-success-emphasis; + -color-button-fg-hover: -color-fg-emphasis; + + -color-button-bg-focused: -color-success-emphasis; + -color-button-fg-focused: -color-fg-emphasis; + + -color-button-bg-pressed: utils.saturate(-color-success-emphasis, cfg.$darkMode, cfg.$color-delta-active); + -color-button-fg-pressed: -color-fg-emphasis; + } + + &.flat { + -color-button-fg: -color-success-emphasis; + -color-button-bg-hover: -color-success-subtle; + } +} + +@mixin danger() { + -color-button-bg: -color-danger-emphasis; + -color-button-fg: -color-fg-emphasis; + -color-button-border: -color-danger-emphasis; + -color-button-border-focused: -color-danger-emphasis; + + &.button-outlined { + -color-button-bg: -color-bg-default; + -color-button-fg: -color-danger-fg; + + -color-button-bg-hover: -color-danger-emphasis; + -color-button-fg-hover: -color-fg-emphasis; + + -color-button-bg-focused: -color-danger-emphasis; + -color-button-fg-focused: -color-fg-emphasis; + + -color-button-bg-pressed: utils.saturate(-color-danger-emphasis, cfg.$darkMode, cfg.$color-delta-active); + -color-button-fg-pressed: -color-fg-emphasis; + } + + &.flat { + -color-button-fg: -color-danger-emphasis; + -color-button-bg-hover: -color-danger-subtle; + } +} + +@mixin flat() { + -color-button-bg: transparent; + -color-button-border: transparent; + + -color-button-bg-hover: -color-bg-subtle; + -color-button-border-hover: transparent; + + -color-button-bg-focused: transparent; + -color-button-border-focused: transparent; + + -color-button-bg-pressed: transparent; + -color-button-border-pressed: transparent; +} + +.button { + @include base(); + + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + + &:hover, + &:hover:focused { + -fx-background-color: -color-button-border-hover, -color-button-bg-hover; + -fx-text-fill: -color-button-fg-hover; + + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-button-fg-hover; + -fx-fill: -color-button-fg-hover; + } + } + + &:focused { + -fx-background-color: -color-button-border-focused, -color-button-bg-focused; + -fx-text-fill: -color-button-fg-focused; + + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-button-fg-focused; + -fx-fill: -color-button-fg-focused; + } + } + + &:armed, + &:focused:armed { + -fx-background-color: -color-button-border-pressed, -color-button-bg-pressed; + -fx-text-fill: -color-button-fg-pressed; + + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-button-fg-pressed; + -fx-fill: -color-button-fg-pressed; + } + } + + &:default, + &.accent { + @include accent(); + } + + &.success { + @include success(); + } + + &.danger { + @include danger(); + } + + &.flat { + @include flat(); + + &:armed, + &:focused:armed { + -fx-underline: true; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_chart.scss b/styles/src/components/_chart.scss new file mode 100755 index 0000000..1060dce --- /dev/null +++ b/styles/src/components/_chart.scss @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +.chart { + -fx-padding: 4px; + + >.chart-title { + -fx-font-size: cfg.$font-title-4; + } + + >.chart-content { + -fx-padding: 10px; + + >.chart-plot-background { + -fx-background-color: -color-bg-default; + } + } + + &:disabled { + >.chart-content { + + // prevent opacity from being applied twice + .label { + -fx-opacity: 1; + } + + -fx-opacity: cfg.$opacity-disabled; + } + } + + >.chart-legend { + -fx-padding: 6px; + } + + .axis { + -fx-axis-color: -color-border-default; + -fx-tick-label-font-size: cfg.$font-small; + -fx-tick-label-fill: -color-fg-default; + + &:top { + -fx-border-color: transparent transparent -fx-axis-color transparent; + } + + &:right { + -fx-border-color: transparent transparent transparent -fx-axis-color; + } + + &:bottom { + -fx-border-color: -fx-axis-color transparent transparent transparent; + } + + &:left { + -fx-border-color: transparent -fx-axis-color transparent transparent; + } + + &:top>.axis-label, + &:left>.axis-label { + -fx-padding: 0 0 4px 0; + } + + &:bottom>.axis-label, + &:right>.axis-label { + -fx-padding: 4px 0 0 0; + } + + >.axis-tick-mark, + >.axis-minor-tick-mark { + -fx-fill: none; + -fx-stroke: -fx-axis-color; + } + } + + .chart-horizontal-grid-lines, + .chart-vertical-grid-lines { + -fx-stroke: -color-border-muted; + -fx-stroke-dash-array: 0.25em, 0.25em; + } + + .chart-alternative-row-fill, + .chart-alternative-column-fill { + -fx-fill: none; + -fx-stroke: none; + } + + .chart-vertical-zero-line, + .chart-horizontal-zero-line { + -fx-stroke: -color-fg-default; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// ScatterChart // +/////////////////////////////////////////////////////////////////////////////// + +// solid circle +.chart-symbol { + -fx-background-color: -color-chart-1; + -fx-background-radius: 5px; + -fx-padding: 5px; +} + +// solid square +.default-color1.chart-symbol { + -fx-background-color: -color-chart-2; + -fx-background-radius: 0; +} + +// solid diamond +.default-color2.chart-symbol { + -fx-background-color: -color-chart-3; + -fx-background-radius: 0; + -fx-padding: 7px 5px 7px 5px; + -fx-shape: "M5,0 L10,9 L5,18 L0,9 Z"; +} + +// cross +.default-color3.chart-symbol { + -fx-background-color: -color-chart-4; + -fx-background-radius: 0; + -fx-background-insets: 0; + -fx-shape: "M2,0 L5,4 L8,0 L10,0 L10,2 L6,5 L10,8 L10,10 L8,10 L5,6 L2,10 L0,10 L0,8 L4,5 L0,2 L0,0 Z"; +} + +// solid triangle +.default-color4.chart-symbol { + -fx-background-color: -color-chart-5; + -fx-background-radius: 0; + -fx-background-insets: 0; + -fx-shape: "M5,0 L10,8 L0,8 Z"; +} + +// hollow circle +.default-color5.chart-symbol { + -fx-background-color: -color-chart-6, white; + -fx-background-insets: 0, 2; + -fx-background-radius: 5px; + -fx-padding: 5px; +} + +// hollow square +.default-color6.chart-symbol { + -fx-background-color: -color-chart-7, white; + -fx-background-insets: 0, 2; + -fx-background-radius: 0; +} + +// hollow diamond +.default-color7.chart-symbol { + -fx-background-color: -color-chart-8, white; + -fx-background-radius: 0; + -fx-background-insets: 0, 2.5; + -fx-padding: 7px 5px 7px 5px; + -fx-shape: "M5,0 L10,9 L5,18 L0,9 Z"; +} + +/////////////////////////////////////////////////////////////////////////////// +// LineChart // +/////////////////////////////////////////////////////////////////////////////// + +.chart-line-symbol { + -fx-background-color: -color-chart-1, white; + -fx-background-insets: 0, 2; + -fx-background-radius: 5px; + -fx-padding: 5px; +} + +.chart-series-line { + -fx-stroke: -color-chart-1; + -fx-stroke-width: 3px; +} + +.default-color0.chart-line-symbol { + -fx-background-color: -color-chart-1, white; +} + +.default-color1.chart-line-symbol { + -fx-background-color: -color-chart-2, white; +} + +.default-color2.chart-line-symbol { + -fx-background-color: -color-chart-3, white; +} + +.default-color3.chart-line-symbol { + -fx-background-color: -color-chart-4, white; +} + +.default-color4.chart-line-symbol { + -fx-background-color: -color-chart-5, white; +} + +.default-color5.chart-line-symbol { + -fx-background-color: -color-chart-6, white; +} + +.default-color6.chart-line-symbol { + -fx-background-color: -color-chart-7, white; +} + +.default-color7.chart-line-symbol { + -fx-background-color: -color-chart-8, white; +} + +.default-color0.chart-series-line { + -fx-stroke: -color-chart-1; +} + +.default-color1.chart-series-line { + -fx-stroke: -color-chart-2; +} + +.default-color2.chart-series-line { + -fx-stroke: -color-chart-3; +} + +.default-color3.chart-series-line { + -fx-stroke: -color-chart-4; +} + +.default-color4.chart-series-line { + -fx-stroke: -color-chart-5; +} + +.default-color5.chart-series-line { + -fx-stroke: -color-chart-6; +} + +.default-color6.chart-series-line { + -fx-stroke: -color-chart-7; +} + +.default-color7.chart-series-line { + -fx-stroke: -color-chart-8; +} + +/////////////////////////////////////////////////////////////////////////////// +// AreaChart // +/////////////////////////////////////////////////////////////////////////////// + +.chart-area-symbol { + -fx-background-color: -color-chart-1, white; + -fx-background-insets: 0, 1; + -fx-background-radius: 4px; // makes sure this remains circular + -fx-padding: 3px; +} + +.default-color0.chart-area-symbol { + -fx-background-color: -color-chart-1, white; +} + +.default-color1.chart-area-symbol { + -fx-background-color: -color-chart-2, white; +} + +.default-color2.chart-area-symbol { + -fx-background-color: -color-chart-3, white; +} + +.default-color3.chart-area-symbol { + -fx-background-color: -color-chart-4, white; +} + +.default-color4.chart-area-symbol { + -fx-background-color: -color-chart-5, white; +} + +.default-color5.chart-area-symbol { + -fx-background-color: -color-chart-6, white; +} + +.default-color6.chart-area-symbol { + -fx-background-color: -color-chart-7, white; +} + +.default-color7.chart-area-symbol { + -fx-background-color: -color-chart-8, white; +} + +.chart-series-area-line { + -fx-stroke: -color-chart-1; + -fx-stroke-width: 1px; +} + +.default-color0.chart-series-area-line { + -fx-stroke: -color-chart-1; +} + +.default-color1.chart-series-area-line { + -fx-stroke: -color-chart-2; +} + +.default-color2.chart-series-area-line { + -fx-stroke: -color-chart-3; +} + +.default-color3.chart-series-area-line { + -fx-stroke: -color-chart-4; +} + +.default-color4.chart-series-area-line { + -fx-stroke: -color-chart-5; +} + +.default-color5.chart-series-area-line { + -fx-stroke: -color-chart-6; +} + +.default-color6.chart-series-area-line { + -fx-stroke: -color-chart-7; +} + +.default-color7.chart-series-area-line { + -fx-stroke: -color-chart-8; +} + +.chart-series-area-fill { + -fx-stroke: none; + -fx-fill: -color-chart-1-alpha20; +} + +.default-color0.chart-series-area-fill { + -fx-fill: -color-chart-1-alpha20; +} + +.default-color1.chart-series-area-fill { + -fx-fill: -color-chart-2-alpha20; +} + +.default-color2.chart-series-area-fill { + -fx-fill: -color-chart-3-alpha20; +} + +.default-color3.chart-series-area-fill { + -fx-fill: -color-chart-4-alpha20; +} + +.default-color4.chart-series-area-fill { + -fx-fill: -color-chart-5-alpha20; +} + +.default-color5.chart-series-area-fill { + -fx-fill: -color-chart-6-alpha20; +} + +.default-color6.chart-series-area-fill { + -fx-fill: -color-chart-7-alpha20; +} + +.default-color7.chart-series-area-fill { + -fx-fill: -color-chart-8-alpha20; +} + +.area-legend-symbol { + -fx-padding: 6px; + -fx-background-radius: 6px; // makes sure this remains circular + -fx-background-insets: 0, 3; +} + +/////////////////////////////////////////////////////////////////////////////// +// BubbleChart // +/////////////////////////////////////////////////////////////////////////////// + +.bubble-legend-symbol { + -fx-background-radius: 8px; + -fx-padding: 8px; +} + +.chart-bubble { + -fx-bubble-fill: -color-chart-1-alpha70; + -fx-background-color: radial-gradient(center 50% 50%, + radius 80%, + derive(-fx-bubble-fill, 20%), + derive(-fx-bubble-fill, -30%)); +} + +.default-color0.chart-bubble { + -fx-bubble-fill: -color-chart-1-alpha70; +} + +.default-color1.chart-bubble { + -fx-bubble-fill: -color-chart-2-alpha70; +} + +.default-color2.chart-bubble { + -fx-bubble-fill: -color-chart-3-alpha70; +} + +.default-color3.chart-bubble { + -fx-bubble-fill: -color-chart-4-alpha70; +} + +.default-color4.chart-bubble { + -fx-bubble-fill: -color-chart-5-alpha70; +} + +.default-color5.chart-bubble { + -fx-bubble-fill: -color-chart-6-alpha70; +} + +.default-color6.chart-bubble { + -fx-bubble-fill: -color-chart-7-alpha70; +} + +.default-color7.chart-bubble { + -fx-bubble-fill: -color-chart-8-alpha70; +} + +/////////////////////////////////////////////////////////////////////////////// +// BarChart // +/////////////////////////////////////////////////////////////////////////////// + +.chart-bar { + -fx-bar-fill: -color-chart-1; + -fx-background-color: linear-gradient(to right, + derive(-fx-bar-fill, -4%), + derive(-fx-bar-fill, -1%), + derive(-fx-bar-fill, 0%), + derive(-fx-bar-fill, -1%), + derive(-fx-bar-fill, -6%)); + -fx-background-insets: 0; +} + +.chart-bar.danger { + -fx-background-insets: 1 0 0 0; +} + +.bar-chart:horizontal .chart-bar { + -fx-background-insets: 0 0 0 1; +} + +.bar-chart:horizontal .chart-bar, +.stacked-bar-chart:horizontal .chart-bar { + -fx-background-color: linear-gradient(to bottom, + derive(-fx-bar-fill, -4%), + derive(-fx-bar-fill, -1%), + derive(-fx-bar-fill, 0%), + derive(-fx-bar-fill, -1%), + derive(-fx-bar-fill, -6%)); +} + +.default-color0.chart-bar { + -fx-bar-fill: -color-chart-1; +} + +.default-color1.chart-bar { + -fx-bar-fill: -color-chart-2; +} + +.default-color2.chart-bar { + -fx-bar-fill: -color-chart-3; +} + +.default-color3.chart-bar { + -fx-bar-fill: -color-chart-4; +} + +.default-color4.chart-bar { + -fx-bar-fill: -color-chart-5; +} + +.default-color5.chart-bar { + -fx-bar-fill: -color-chart-6; +} + +.default-color6.chart-bar { + -fx-bar-fill: -color-chart-7; +} + +.default-color7.chart-bar { + -fx-bar-fill: -color-chart-8; +} + +.bar-legend-symbol { + -fx-padding: 8px; +} + +/////////////////////////////////////////////////////////////////////////////// +// PieChart // +/////////////////////////////////////////////////////////////////////////////// + +.chart-pie { + -fx-pie-color: -color-chart-1; + -fx-background-color: radial-gradient(radius 100%, + derive(-fx-pie-color, 20%), + derive(-fx-pie-color, -10%)); + -fx-background-insets: 1; + -fx-border-color: -color-bg-default; +} + +.chart-pie-label { + -fx-padding: 3px; + -fx-fill: -color-fg-default; +} + +.chart-pie-label-line { + -fx-stroke: derive(-color-bg-default, -20%); +} + +.default-color0.chart-pie { + -fx-pie-color: -color-chart-1; +} + +.default-color1.chart-pie { + -fx-pie-color: -color-chart-2; +} + +.default-color2.chart-pie { + -fx-pie-color: -color-chart-3; +} + +.default-color3.chart-pie { + -fx-pie-color: -color-chart-4; +} + +.default-color4.chart-pie { + -fx-pie-color: -color-chart-5; +} + +.default-color5.chart-pie { + -fx-pie-color: -color-chart-6; +} + +.default-color6.chart-pie { + -fx-pie-color: -color-chart-7; +} + +.default-color7.chart-pie { + -fx-pie-color: -color-chart-8; +} + +.danger.chart-pie { + -fx-pie-color: transparent; + -fx-background-color: white; +} + +.pie-legend-symbol.chart-pie { + -fx-background-radius: 8px; + -fx-padding: 8px; + -fx-border-color: none; +} \ No newline at end of file diff --git a/styles/src/components/_checkbox.scss b/styles/src/components/_checkbox.scss new file mode 100755 index 0000000..202de87 --- /dev/null +++ b/styles/src/components/_checkbox.scss @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/icons"; + +.check-box { + + // applied to label + -fx-text-fill: -color-fg-default; + -fx-label-padding: cfg.$checkbox-label-padding cfg.$checkbox-label-padding cfg.$checkbox-label-padding cfg.$graphic-gap; + + >.box { + -fx-background-color: -color-fg-default, -color-bg-default; + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: cfg.$border-radius; + -fx-padding: cfg.$checkbox-mark-padding-y cfg.$checkbox-mark-padding-x cfg.$checkbox-mark-padding-y cfg.$checkbox-mark-padding-x; + -fx-alignment: CENTER; + + >.mark { + -fx-background-color: -color-bg-default; // mark is hidden + @include icons.get("check", true); + -fx-min-height: cfg.$checkbox-mark-size; + -fx-max-height: cfg.$checkbox-mark-size; + -fx-min-width: cfg.$checkbox-mark-size; + -fx-max-width: cfg.$checkbox-mark-size; + } + } + + &:indeterminate { + >.box { + >.mark { + -fx-background-color: -color-fg-muted; + @include icons.get("minus", false); + } + } + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + + >.box { + -fx-opacity: cfg.$opacity-disabled; + } + } + + &:selected { + >.box { + -fx-background-color: -color-accent-emphasis, -color-accent-emphasis; + + >.mark { + -fx-background-color: -color-fg-emphasis; + } + } + } + + &:show-mnemonics>.mnemonic-underline { + -fx-stroke: -color-fg-default; + } +} \ No newline at end of file diff --git a/styles/src/components/_color-picker.scss b/styles/src/components/_color-picker.scss new file mode 100755 index 0000000..3a9df85 --- /dev/null +++ b/styles/src/components/_color-picker.scss @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/effects"; + +// combo box +.color-picker { + + >.color-picker-label { + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + + >.label { + -fx-text-fill: -color-fg-default; + } + + >.picker-color { + >.picker-color-rect { + -fx-stroke: -color-border-default; + } + } + } + + // ColorPicker.STYLE_CLASS_BUTTON + &.button { + >.color-picker-label { + -fx-padding: 0; + } + } +} + +// popup window +.color-palette { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: cfg.$border-radius; + -fx-spacing: 10px; + -fx-padding: 1em; + + >.color-picker-grid { + -fx-padding: 0.5px; + -fx-snap-to-pixel: false; + + >.color-square { + -fx-background-color: transparent; + -fx-padding: 0.5px; + } + } +} + +// this is another popup window, it's not inside the .color-patette +// also note that it's applied to both palette dropdown popup AND +// each individual color hover popup +.color-palette-region { + // color popup window positioning, + // transparent is fit both light and dark modes + -fx-effect: dropshadow(gaussian, transparent, 12, 0, 0, 8); + + // the color over which the user is hovering + >.color-square.hover-square { + -fx-background-color: -color-accent-fg, -color-bg-default; + -fx-background-insets: -2, -1; // border width + -fx-background-radius: 5, 0; + -fx-scale-x: 1.5; // magnification + -fx-scale-y: 1.5; + -fx-border-color: -color-accent-fg; + -fx-border-insets: -1, -1; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// CustomColorDialog // +/////////////////////////////////////////////////////////////////////////////// + +.custom-color-dialog { + -fx-background-color: -color-bg-default; + -fx-padding: 1.25em; + -fx-spacing: 1.25em; + + >.color-rect-pane { + -fx-spacing: 1em; + -fx-pref-height: 16em; + -fx-alignment: TOP-LEFT; + -fx-fill-height: true; + + >.color-rect { + -fx-min-width: 16em; + -fx-min-height: 16em; + + .color-rect-border { + -fx-border-color: -color-border-default; + } + + #color-rect-indicator { + -fx-background-color: none; + -fx-border-color: white; // circular indicator + -fx-border-radius: 0.4166667em; + -fx-pref-width: 0.833333em; + -fx-pref-height: 0.833333em; + -fx-translate-x: -0.4166667em; + -fx-translate-y: -0.4166667em; + -fx-effect: dropshadow(three-pass-box, black, 2, 0.0, 0, 1); + } + } + + >.color-bar { + -fx-min-width: 1.666667em; + -fx-min-height: 16.666667em; + -fx-max-width: 1.666667em; + -fx-border-color: -color-border-default; + + #color-bar-indicator { + -fx-border-radius: 0.333333em; + -fx-border-color: white; // rect indicator + -fx-pref-width: 2em; + -fx-pref-height: 0.833333em; + -fx-translate-x: -0.1666667em; + -fx-translate-y: -0.4166667em; + -fx-effect: dropshadow(three-pass-box, black, 2, 0.0, 0, 1); + } + } + } + + >.controls-pane { + >.current-new-color-grid { + >.label { + -fx-padding: 0 0 0 calc(cfg.$border-width * 2); + } + + >#current-new-color-border { + -fx-border-color: -color-border-default; + -fx-border-width: cfg.$border-width; + } + + >.color-rect { + -fx-min-width: 10em; + -fx-pref-width: 10em; + + -fx-min-height: 1.75em; + -fx-pref-height: 1.75em; + } + + // top spacer (between labels and color rect) + >#spacer1 { + -fx-min-height: 5px; + -fx-pref-height: 5px; + -fx-max-height: 5px; + } + + // bottom spacer (between color rect and settings pane) + >#spacer2 { + -fx-min-height: 1em; + -fx-pref-height: 1em; + -fx-max-height: 1em; + } + } + + #settings-pane { + -fx-hgap: 6px; + -fx-vgap: 6px; + + >.customcolor-controls-background { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: calc(14px - cfg.$border-width) 0 calc(6px - cfg.$border-width) 0, + 14px cfg.$border-width 6px cfg.$border-width; + -fx-background-radius: cfg.$border-radius; + } + + >.settings-label { + -fx-min-width: 5.75em; + } + + >.settings-unit { + -fx-min-width: 1.5em; + -fx-pref-width: 1.5em; + -fx-max-width: 1.5em; + } + + >.slider { + -fx-pref-width: 10em; + } + + >.color-input-field { + -fx-max-width: 4em; + -fx-pref-width: 4em; + -fx-min-width: 4em; + -fx-pref-column-count: 3; + } + + >#spacer-side { + -fx-min-width: 0.5em; + -fx-pref-width: 0.5em; + } + + >#spacer-bottom { + -fx-min-height: 1em; + -fx-pref-height: 1em; + } + + >.web-field { + -fx-pref-column-count: 6; + -fx-pref-width: 8em; + } + + // keeps the text right-aligned when in RTL mode + >.webcolor-field:dir(rtl)>.text-field:dir(ltr) { + -fx-alignment: BASELINE_RIGHT; + } + } + + >#buttons-hbox { + -fx-spacing: 10px; + -fx-padding: 1em 0 0 0; + -fx-alignment: BOTTOM_RIGHT; + } + + .transparent-pattern { + -fx-background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpCOTBEQkE1RjJFMjA2ODExOUExMUM5NDhFOTUyQzM3MCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGRkE3MDZERThFNUYxMUUxQjU5RUNFQTE3OTA1RDFFMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGRkE3MDZERDhFNUYxMUUxQjU5RUNFQTE3OTA1RDFFMSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDk4MDExNzQwNzIwNjgxMTg3MUZDMUExNDFCMTYwNzkiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjkwREJBNUYyRTIwNjgxMTlBMTFDOTQ4RTk1MkMzNzAiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz71FDCdAAAAKElEQVR42mI8c+YMAwwYGxvD2UwMOADpEoz///+Hc86ePUsLOwACDABC1ghwV8TLOQAAAABJRU5ErkJggg=="); + -fx-background-repeat: repeat; + -fx-background-size: auto; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_combo-box.scss b/styles/src/components/_combo-box.scss new file mode 100755 index 0000000..434f4cc --- /dev/null +++ b/styles/src/components/_combo-box.scss @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/effects"; +@use "../settings/icons"; +@use "../settings/utils"; + +@mixin _arrow() { + @include icons.get("arrow-drop-down", false); + -fx-background-color: -color-fg-muted; +} + +@mixin _combo-box-base() { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, 1; + -fx-background-radius: cfg.$border-radius; + -fx-text-fill: -color-fg-default; + -fx-alignment: CENTER; + -fx-content-display: LEFT; + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + + &:success, + &:success:focused { + -fx-background-color: -color-success-emphasis, -color-bg-default; + } + + &:danger, + &:danger:focused { + -fx-background-color: -color-danger-emphasis, -color-bg-default; + } + + &:focused { + -fx-background-color: -color-accent-emphasis, -color-bg-default; + } + + // input group + &.left-pill { + -fx-background-radius: cfg.$border-radius 0 0 cfg.$border-radius; + -fx-background-insets: 0, cfg.$border-width 0 cfg.$border-width cfg.$border-width; + + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } + + &.center-pill { + -fx-background-radius: 0; + -fx-background-insets: 0, cfg.$border-width 0 cfg.$border-width 0; + + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } + + &.right-pill { + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-background-insets: 0, cfg.$border-width cfg.$border-width cfg.$border-width 0; + + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// ComboBox // +/////////////////////////////////////////////////////////////////////////////// + +// .combo-box-base is only applied to the ComboBox, ColorPicker and Datepicker, +// it's not applied to the ChoiceBox +.combo-box-base { + + @include _combo-box-base(); + + >.arrow-button { + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + + >.arrow { + @include _arrow(); + } + } + + >.text-field { + -fx-background-insets: 0, 1 0 1 1; + -fx-background-radius: cfg.$border-radius 0 0 cfg.$border-radius; + } + + &:success { + >.arrow-button>.arrow { + -fx-background-color: -color-success-fg; + } + } + + &:danger { + >.arrow-button>.arrow { + -fx-background-color: -color-danger-fg; + } + } +} + +.combo-box { + + // customise the ListCell that appears in the ComboBox button itself + >.list-cell { + -fx-background-color: transparent; + -fx-text-fill: -color-fg-default; + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + -fx-graphic-text-gap: cfg.$graphic-gap; + } + + &:success>.list-cell { + -fx-text-fill: -color-success-fg; + } + + &:danger>.list-cell { + -fx-text-fill: -color-danger-fg; + } +} + +.combo-box-popup { + >.list-view { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, 1; + -fx-background-radius: cfg.$border-radius; + + >.virtual-flow { + >.clipped-container { + >.sheet { + >.list-cell { + // reset cell size, because height is set via paddings + // to use the same values as ChoiceBox + -fx-cell-size: 0; + -fx-background-color: -color-bg-default; + -fx-padding: cfg.$menu-padding-y cfg.$menu-padding-x cfg.$menu-padding-y cfg.$menu-padding-x; + -fx-graphic-text-gap: cfg.$graphic-gap; + + &:filled:hover { + -fx-background-color: utils.saturate(-color-bg-default, cfg.$darkMode, cfg.$color-delta-hover); + } + + &:filled:selected, + &:filled:selected:hover { + -fx-background-color: -color-accent-subtle; + } + } + } + } + } + + // placeholder is shown to the user when the ComboBox has no content to show + >.placeholder { + >.label { + -fx-text-fill: -color-fg-muted; + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// ChoiceBox // +/////////////////////////////////////////////////////////////////////////////// + +.choice-box { + + @include _combo-box-base(); + + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + + >.label { + -fx-text-fill: -color-fg-default; + } + + >.open-button { + + >.arrow { + @include _arrow(); + } + } + + &:success { + >.label { + -fx-text-fill: -color-success-fg; + } + + >.open-button>.arrow { + -fx-background-color: -color-success-fg; + } + } + + &:danger { + >.label { + -fx-text-fill: -color-danger-fg; + } + + >.open-button>.arrow { + -fx-background-color: -color-danger-fg; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_custom-text-field.scss b/styles/src/components/_custom-text-field.scss new file mode 100644 index 0000000..4284ff2 --- /dev/null +++ b/styles/src/components/_custom-text-field.scss @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "sass:math"; + +// space bethween text and custom node +$text-gap: 4px !default; + +.custom-text-field { + + &:left-node-visible { + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y 0; + + .left-pane { + -fx-padding: 0 $text-gap 0 math.div(cfg.$padding-x, 2); + } + } + + &:right-node-visible { + -fx-padding: cfg.$padding-y 0 cfg.$padding-y cfg.$padding-x; + + .right-pane { + -fx-padding: 0 math.div(cfg.$padding-x, 2) 0 $text-gap; + } + } + + &:left-node-visible:right-node-visible { + -fx-padding: cfg.$padding-y 0 cfg.$padding-y 0; + } + + &:success { + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-success-fg; + -fx-fill: -color-success-fg; + } + } + + &:danger { + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-danger-fg; + -fx-fill: -color-danger-fg; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_data.scss b/styles/src/components/_data.scss new file mode 100755 index 0000000..00cfd87 --- /dev/null +++ b/styles/src/components/_data.scss @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/icons"; + +$cell-size-normal: 2.8em !default; +$cell-size-dense: 2em !default; +$cell-padding-x: 0.5em !default; + +// .tree-cell doesn't support -fx-cell-size +// its height should be set via vertical paddings +$tree-cell-padding-x: $cell-padding-x !default; +$tree-cell-padding-y: $cell-padding-x !default; +$tree-cell-indent: 1em !default; + +@mixin _base() { + + -fx-border-color: -color-border-default; + -fx-border-width: cfg.$border-width; + -fx-border-radius: 0; + + >.virtual-flow { + >.corner { + -fx-background-color: -color-border-subtle; + -fx-opacity: cfg.$opacity-disabled; + } + + // apply opacity to all but control borders + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + } +} + +// row selection (cellSelectionEnabled = false) +.list-view:focused>.virtual-flow>.clipped-container>.sheet>.list-cell:filled:selected, +.tree-view:focused>.virtual-flow>.clipped-container>.sheet>.tree-cell:filled:selected, +.table-view:focused>.virtual-flow>.clipped-container>.sheet>.table-row-cell:filled:selected, +.tree-table-view:focused>.virtual-flow>.clipped-container>.sheet>.tree-table-row-cell:filled:selected { + -fx-background-color: -color-border-default, -color-accent-subtle; +} + +// individual cell selection (cellSelectionEnabled = true) +.table-view:focused>.virtual-flow>.clipped-container>.sheet>.table-row-cell .table-cell:selected, +.tree-table-view:focused>.virtual-flow>.clipped-container>.sheet>.tree-table-row-cell .tree-table-cell:selected { + -fx-background-color: -color-accent-subtle; + // a margin to show bottom .table-row-cell border, + // it should be 1px height, but for some reason it's not enough + -fx-background-insets: 0 0 2 0; +} + +/////////////////////////////////////////////////////////////////////////////// +// Customised CSS for controls placed directly within cells // +/////////////////////////////////////////////////////////////////////////////// + +// nested controls supposed to have nor background nor borders +.cell { + + // NOTE: + // The controls inside .tree-cell _with graphic_ will be wrapped into additional + // container that don't use hgrow and adjusts its width after showing popup. + // It looks rather ugly, but there's nothing could be fixed with CSS. + // That's also why we don't use child combinator here. + // #javafx-bug + + .text-input { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-padding: 0; + } + + .check-box { + -fx-padding: 0 cfg.$graphic-gap 0 0; + } + + .choice-box { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-padding: 0 cfg.$padding-x 0 0; + -fx-alignment: CENTER_LEFT; + -fx-content-display: LEFT; + } + + .combo-box { + -fx-background-color: transparent; + -fx-alignment: CENTER_LEFT; + -fx-content-display: LEFT; + -fx-background-radius: 0; + + // must be more spefific than ".list-view .list-cell" selector (see below) + .cell.list-cell { + -fx-background-color: transparent; + -fx-padding: 0; + -fx-background-insets: 0; + -fx-background-radius: 0; + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// ListView // +/////////////////////////////////////////////////////////////////////////////// + +.list-view { + @include _base(); + + .list-cell { + -fx-background-color: -color-bg-default; + -fx-text-fill: -color-fg-default; + -fx-padding: 0 $cell-padding-x 0 $cell-padding-x; + -fx-cell-size: $cell-size-normal; + + // there's no ":first" or ":last" cell pseudo classes, + // so we can't avoid double border at the bottom + -fx-border-width: 0 0 1 0; + -fx-border-color: transparent; + } + + &.bordered { + .list-cell { + -fx-border-color: -color-border-default; + } + } + + &.dense { + .list-cell { + -fx-cell-size: $cell-size-dense; + } + } + + &.striped { + .list-cell:odd { + -fx-background-color: -color-bg-inset; + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// TableView // +/////////////////////////////////////////////////////////////////////////////// + +@mixin _generic-table { + + @include _base(); + + &.bordered { + >.column-header-background { + .column-header { + -fx-background-color: -color-border-default, -color-bg-inset; + -fx-background-insets: 0, 0 1 0 0; + } + } + } + + // the column header row is made up of a number of .column-header, one for each TableColumn + >.column-header-background { + + -fx-background-color: -color-border-default, -color-bg-inset; + -fx-background-insets: 0, 0 0 1 0; + + // the only way to draw bottom header border is to use .table-column + // .column-header won't work nonetheless both classes applied to the same node + .table-column { + -fx-border-color: -color-border-default; + -fx-border-width: 0 0 1 0; + } + + // columns headers can be nested, so there's no child combinator here + .column-header { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-size: 2.2em; + -fx-padding: 0; + -fx-font-weight: bold; + + // any label within column header, including title and sort order label + .label { + -fx-text-fill: -color-fg-default; + -fx-alignment: CENTER; + } + + // sort container + GridPane { + -fx-padding: 0 4px 0 0; + } + + // column sort arrows + .arrow { + -fx-background-color: -color-fg-muted; + -fx-padding: 3px 4px 3px 4px; + -fx-shape: "M 0 0 h 7 l -3.5 4 z"; + } + + // dots are used to indicate up to 3 multiple sort columns + .sort-order-dots-container { + -fx-padding: 2px 0 2px 0; + + >.sort-order-dot { + -fx-background-color: -color-fg-muted; + -fx-padding: 0.115em; + -fx-background-radius: 0.115em; + } + } + + // digits are used to indicate more than 3 multiple sort columns + .sort-order { + -fx-padding: 0 0 0 2px; + } + } + + // .filler area extends from the right-most column to the edge of the table view + >.filler { + -fx-background-color: transparent; + -fx-border-color: -color-border-default; + -fx-border-width: 0 0 1 0; + } + + // table menu button + >.show-hide-columns-button { + -fx-border-color: -color-border-default; + -fx-border-width: 0 0 1 0; + -fx-cursor: hand; + + // NOTE: + // If you want to increase right margin you should take into account + // that it's harder than it looks. E.g. paddings, transparent insets or borders + // aren't working. Just be sure you have enough time to tackle this problem. + + >.show-hide-column-image { + -fx-background-color: -color-fg-muted; + @include icons.get("more-vert", true); + -fx-padding: 0.4em 0.115em 0.4em 0.115em; + } + } + } + + // the .column-resize-line is shown when the user is attempting to resize a column + .column-resize-line { + -fx-background-color: -color-accent-emphasis; + -fx-padding: 0 1 0 1; + } + + // when a column is being dragged to be placed in a different position, there is a region + // that follows along the column header area to indicate where the column will be dropped + .column-drag-header { + // -color-accent-muted must be RGBA color for this to work, because + // applying opacity to the whole pane makes label text unreadable + -fx-background-color: -color-accent-muted; + } + + // semi-transparent overlay to indicate the column that is currently being moved + .column-overlay { + -fx-background-color: -color-accent-muted; + } + + // this is shown when the table has no rows and/or no columns + .placeholder>.label { + -fx-background-color: -color-bg-default; + -fx-font-size: cfg.$font-title-4; + } +} + +.table-view { + + @include _generic-table(); + + &.bordered { + .table-row-cell>.table-cell { + -fx-border-color: transparent -color-border-default transparent transparent; + + &:empty { + -fx-border-color: transparent; + } + } + } + + &.dense { + .table-row-cell { + -fx-cell-size: $cell-size-dense; + } + } + + &.striped { + .table-row-cell:odd { + -fx-background-color: -color-border-default, -color-bg-inset; + } + } + + // each row in the table is a .table-row-cell, + // inside a .table-row-cell is any number of .table-cell + .table-row-cell { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, 0 0 1 0; + -fx-padding: 0; + -fx-cell-size: $cell-size-normal; + + // hide empty rows + &:empty { + -fx-background-color: transparent; + -fx-background-insets: 0; + + >.table-cell { + -fx-border-color:transparent; + } + } + + >.table-cell { + -fx-padding: 0 $cell-padding-x 0 $cell-padding-x; + -fx-text-fill: -color-fg-default; + -fx-alignment: CENTER_LEFT; + } + } +} + +// when in constrained resize mode, the right-most visible cell should not have a right-border, +// as it is not possible to get this cleanly out of view without introducing horizontal scrollbars +.table-view:constrained-resize>.virtual-flow>.clipped-container>.sheet>.table-row-cell>.table-cell:last-visible, +.tree-table-view:constrained-resize>.virtual-flow>.clipped-container>.sheet>.tree-table-row-cell>.tree-table-cell:last-visible { + -fx-border-color: transparent; +} + +// reset inherited font weight for context menu +.table-view .column-header .context-menu, +.tree-table-view .column-header .context-menu, +.table-view>.column-header-background>.show-hide-columns-button .context-menu, +.tree-table-view>.column-header-background>.show-hide-columns-button .context-menu { + -fx-font-weight: null; +} + +// table cells +.table-view .table-row-cell>.table-cell.check-box-table-cell, +.table-view .table-row-cell>.table-cell.font-icon-table-cell, +.tree-table-view .tree-table-row-cell>.tree-table-cell.check-box-tree-table-cell { + -fx-alignment: CENTER; + -fx-padding: 0; +} + +/////////////////////////////////////////////////////////////////////////////// +// TreeView // +/////////////////////////////////////////////////////////////////////////////// + +.tree-view { + @include _base(); +} + +.tree-cell { + -fx-background-color: -color-bg-default; + -fx-text-fill: -color-fg-default; + -fx-padding: $tree-cell-padding-y 0 $tree-cell-padding-y 0; + -fx-indent: $tree-cell-indent; + + // NOTE: + // Instead of simply applying vertical alignment programmatically the TreeView + // expects you will do the same thing by adjusting .tree-disclosure-node + // paddings manually ¯\_(ツ)_/¯. The below values are adjusted for the theme + // default font size (14px). If you change the font size, you're supposed to + // re-adjust paddings as well. Also learn about TreeCellSkin#maxDisclosureWidthMap. + // #javafx-bug + >.tree-disclosure-node { + -fx-padding: 4px $tree-cell-padding-x 4px $tree-cell-padding-x; + -fx-background-color: transparent; + } +} + +.tree-cell>.tree-disclosure-node>.arrow, +.tree-table-row-cell>.tree-disclosure-node>.arrow { + @include icons.get("arrow-right", false); + -fx-background-color: -color-fg-default; + -fx-padding: 0.333333em 0.229em 0.333333em 0.229em; +} + +.tree-cell:expanded>.tree-disclosure-node>.arrow, +.tree-table-row-cell:expanded>.tree-disclosure-node>.arrow { + @include icons.get("arrow-drop-down", false); +} + +/////////////////////////////////////////////////////////////////////////////// +// TreeTableView // +/////////////////////////////////////////////////////////////////////////////// + +// NOTE: +// TreeTableView doesn't play well with editable cells. Whatever column +// you use as first, if it's not ordinary TreeTableCell, there will always +// be some issue with tree disclosure node: alignment, size etc. +// There's nothing that can be fixed with CSS. +.tree-table-view { + + @include _generic-table(); + + &.bordered { + .tree-table-row-cell>.tree-table-cell { + -fx-border-color: transparent -color-border-default transparent transparent; + + &:empty { + -fx-border-color: transparent; + } + } + } + + &.dense { + .tree-table-row-cell { + -fx-cell-size: $cell-size-dense; + + >.tree-disclosure-node { + -fx-padding: 0.6em $tree-cell-padding-x 0 $tree-cell-padding-x; + } + } + } + + &.striped { + .tree-table-row-cell:odd { + -fx-background-color: -color-border-default, -color-bg-inset; + } + } + + .tree-table-row-cell { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, 0 0 1 0; + -fx-padding: 0; + -fx-cell-size: $cell-size-normal; + -fx-indent: 1em; + + // hide empty rows + &:empty { + -fx-background-color: transparent; + -fx-background-insets: 0; + } + + // read above about disclosure node alignment + >.tree-disclosure-node { + -fx-padding: 1em $tree-cell-padding-x 0 $tree-cell-padding-x; + -fx-background-color: transparent; + } + + >.tree-table-cell { + -fx-padding: 0 $cell-padding-x 0 $cell-padding-x; + -fx-text-fill: -color-fg-default; + -fx-alignment: CENTER_LEFT; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_date-picker.scss b/styles/src/components/_date-picker.scss new file mode 100755 index 0000000..f2f93ed --- /dev/null +++ b/styles/src/components/_date-picker.scss @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/effects"; +@use "../settings/icons"; + +$content-padding-x: 8px !default; +$content-padding-y: 8px !default; + +$month-year-pane-color-bg: -color-bg-default !default; +$month-year-pane-color-fg: -color-fg-default !default; +$month-year-pane-font-size: 1.1em !default; + +$cell-padding-x: 4px !default; +$cell-padding-y: 8px !default; +$day-name-font-size: 0.9em !default; + +// secondary chronology +$chronology-fg: -color-success-fg !default; +$chronology-label-margin: 0.5em !default; +$chronology-cell-size: 2.75em !default; +$chronology-cell-padding: 0.083333em $cell-padding-x 0.083333em 0.333333em !default; + +.combo-box-base.date-picker { + >.arrow-button { + -fx-cursor: hand; + + >.arrow { + @include icons.get("calendar", true); + -fx-background-color: -color-fg-default; + -fx-padding: 0.416667em; // icon size + } + } +} + +.date-picker-popup { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, 1; + -fx-background-radius: 0; + -fx-alignment: CENTER; + -fx-spacing: 0; + -fx-padding: cfg.$border-width; + + >.month-year-pane { + -fx-padding: $content-padding-y $content-padding-x $content-padding-y $content-padding-x; + -fx-background-color: $month-year-pane-color-bg; + -fx-background-insets: 0; + + // this one is actually HBox, but because of the class name it uses Spinner styles + >.spinner { + -fx-spacing: 4px; + -fx-alignment: CENTER; + -fx-fill-height: false; + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-font-size: $month-year-pane-font-size; + + >.button { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-cursor: hand; + + >.left-arrow { + @include icons.get("chevron-left", false); + -fx-background-color: $month-year-pane-color-fg; + } + + >.right-arrow { + @include icons.get("chevron-right", false); + -fx-background-color: $month-year-pane-color-fg; + } + } + + >.label { + -fx-alignment: CENTER; + -fx-text-fill: $month-year-pane-color-fg; + } + } + + >.secondary-label { + -fx-alignment: BASELINE_CENTER; + -fx-padding: $chronology-label-margin 0 0 0; + -fx-text-fill: $month-year-pane-color-fg; + } + } + + >.calendar-grid { + -fx-background-color: -color-bg-default; + -fx-padding: $content-padding-x; + + >.date-cell { + -fx-background-color: transparent; + -fx-padding: 0; + -fx-alignment: BASELINE_CENTER; + -fx-opacity: 1.0; + -fx-text-fill: -color-fg-default; + } + + >.week-number-cell { + -fx-padding: $cell-padding-y $cell-padding-x $cell-padding-y $cell-padding-x; + -fx-background-color: -color-bg-default; + -fx-text-fill: -color-accent-fg; + -fx-font-size: $day-name-font-size; + } + + >.day-cell { + -fx-padding: $cell-padding-y $cell-padding-x $cell-padding-y $cell-padding-x; + -fx-background-color: -color-bg-default; + + >.secondary-text { + -fx-fill: $chronology-fg; + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + } + + .day-name-cell { + -fx-padding: $cell-padding-y $cell-padding-x $cell-padding-y $cell-padding-x; + -fx-font-size: $day-name-font-size; + } + + >.hijrah-day-cell { + -fx-alignment: TOP_LEFT; + -fx-padding: $chronology-cell-padding; + -fx-cell-size: $chronology-cell-size; + } + + >.today { + -fx-background-color: -color-accent-subtle; + -fx-text-fill: -color-accent-fg; + -fx-font-weight: bold; + } + } +} + +.inline-date-picker { + -fx-effect: none; + + >.month-year-pane { + -fx-alignment: CENTER; + -fx-spacing: 10px; + + >.button { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-cursor: hand; + } + + >.back-button { + >.left-arrow { + @include icons.get("chevron-left", false); + -fx-background-color: $month-year-pane-color-fg; + } + } + + >.forward-button { + >.right-arrow { + @include icons.get("chevron-right", false); + -fx-background-color: $month-year-pane-color-fg; + } + } + + >.label { + -fx-text-fill: $month-year-pane-color-fg; + -fx-font-size: $month-year-pane-font-size; + } + } + + &:disabled { + >.calendar-grid { + -fx-opacity: cfg.$opacity-disabled; + + // prevent opacity from being applied twice + >.day-cell { + &:disabled { + -fx-opacity: 1.0; + } + } + } + } +} + +.date-picker-popup>.calendar-grid>.selected, +.date-picker-popup>.calendar-grid>.selected>.secondary-text, +.date-picker-popup>.calendar-grid>.previous-month.selected, +.date-picker-popup>.calendar-grid>.next-month.selected { + -fx-background-color: -color-accent-emphasis; + -fx-text-fill: -color-fg-emphasis; + -fx-fill: -color-fg-emphasis; + -fx-font-weight: normal; +} + +.date-picker-popup>.calendar-grid>.day-cell:hover { + -fx-background-color: -color-bg-subtle; +} + +.date-picker-popup>.calendar-grid>.selected:hover { + -fx-background-color: -color-accent-emphasis; +} + +.date-picker-popup>.calendar-grid>.previous-month, +.date-picker-popup>.calendar-grid>.next-month, +.date-picker-popup>.calendar-grid>.previous-month.today, +.date-picker-popup>.calendar-grid>.next-month.today, +.date-picker-popup>.calendar-grid>.previous-month>.secondary-text, +.date-picker-popup>.calendar-grid>.next-month>.secondary-text { + -fx-text-fill: -color-fg-muted; + -fx-fill: -color-fg-muted; + -fx-font-weight: normal; +} \ No newline at end of file diff --git a/styles/src/components/_dialog.scss b/styles/src/components/_dialog.scss new file mode 100755 index 0000000..6858f48 --- /dev/null +++ b/styles/src/components/_dialog.scss @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +$padding-x: 1em !default; +$padding-y: 1em !default; + +$image-info: url("data:image/png;base64,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") !default; +$image-warning: url("data:image/png;base64,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") !default; +$image-error: url("data:image/png;base64,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") !default; +$image-confirm: url("data:image/png;base64,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") !default; + +.dialog-pane { + -fx-background-color: -color-bg-default; + -fx-padding: 0; + -fx-max-width: 600px; + + >.expandable-content { + -fx-padding: $padding-y $padding-x $padding-y $padding-x; + } + + >.button-bar>.container { + -fx-padding: calc($padding-y * 2) $padding-x $padding-y $padding-x; + + >.details-button { + -fx-padding: 0; + -fx-alignment: BASELINE_LEFT; + -fx-focus-traversable: false; + -fx-text-fill: -color-fg-default; + + &:hover { + -fx-underline: true; + } + } + } + + >.content { + -fx-padding: $padding-y $padding-x 0 $padding-x; + + &.label { + -fx-alignment: TOP_LEFT; + } + } + + &:header { + >.header-panel { + -fx-padding: $padding-y $padding-x $padding-y $padding-x; + -fx-background-color: -color-border-default, -color-bg-inset; + -fx-background-insets: 0, 0 0 cfg.$border-width 0; + + >.label { + -fx-wrap-text: true; + } + + >.graphic-container { + // this prevents the text in the header running directly into the graphic + -fx-padding: 0 0 0 $padding-x; + } + } + } + + &:no-header { + + >.content { + -fx-padding: $padding-y $padding-x 0 0; + } + + >*>.graphic-container { + -fx-padding: $padding-y $padding-x 0 $padding-x; + } + } + + &.information>.header-panel { + -fx-background-color: -color-accent-fg, -color-bg-subtle; + + >.label { + -fx-text-fill: -color-fg-default; + } + } + + &.warning>.header-panel { + -fx-background-color: -color-warning-fg, -color-bg-subtle; + + >.label { + -fx-text-fill: -color-fg-default; + } + } + + &.error>.header-panel { + -fx-background-color: -color-danger-fg, -color-bg-subtle; + + >.label { + -fx-text-fill: -color-fg-default; + } + } +} + +.alert.information.dialog-pane { + -fx-graphic: $image-info; +} + +.alert.warning.dialog-pane { + -fx-graphic: $image-warning; +} + +.alert.error.dialog-pane { + -fx-graphic: $image-error; +} + +.alert.confirmation.dialog-pane, +.text-input-dialog.dialog-pane, +.choice-dialog.dialog-pane { + -fx-graphic: $image-confirm; +} \ No newline at end of file diff --git a/styles/src/components/_html-editor.scss b/styles/src/components/_html-editor.scss new file mode 100755 index 0000000..098dfe5 --- /dev/null +++ b/styles/src/components/_html-editor.scss @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/utils"; +@use "sass:math"; + +$color-rect-size: 8px !default; + +.html-editor { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, cfg.$border-width; + -fx-padding: calc(cfg.$border-width + 1px); + + &:contains-focus { + -fx-background-color: -color-accent-emphasis, -color-bg-default; + } + + .tool-bar { + -fx-padding: 4px; + } + + .button, + .toggle-button { + -fx-background-insets: 0; + } + + .toggle-button { + -color-button-bg-selected: utils.saturate(-color-button-bg, cfg.$darkMode, cfg.$color-delta-active); + -color-button-border-focused: transparent; + } +} + +.color-picker { + &.html-editor-foreground { + -fx-color-rect-x: 0; + -fx-color-rect-y: -#{math.div($color-rect-size, 2)}; + -fx-color-rect-width: $color-rect-size; + -fx-color-rect-height: $color-rect-size; + -fx-color-label-visible: false; + } + + &.html-editor-background { + -fx-color-rect-x: 0; + -fx-color-rect-y: -#{math.div($color-rect-size, 2)}; + -fx-color-rect-width: $color-rect-size; + -fx-color-rect-height: $color-rect-size; + -fx-color-label-visible: false; + } + + &.html-editor-foreground>.color-picker-label>.picker-color>.picker-color-rect, + &.html-editor-background>.color-picker-label>.picker-color>.picker-color-rect { + -fx-stroke: none; + } +} + +// Sadly JavaFX devs have a mania to declare everyting private and final. +// The below code isn't working, while there are no runtime errors and URLs are correct +// it's still not loaded for unknown reason. +// There are to many images here and HMTLEditor itself is obsolete type of control, +// so I don't want to use data-url. It's either be fixed at OpenJFX or not. +// Anyone who treats this as a problem can easily fix it by using addtitional CSS file. +// Just copy CSS rules from below and images from the OpenJFX repo and then use relative +// URLs. #javafx-bug +$image-path: "/com/sun/javafx/scene/control/skin/modena" !default; + +.color-picker.html-editor-foreground { + -fx-graphic: url("#{$image-path}/HTMLEditor-Text-Color.png"); +} + +.color-picker.html-editor-background { + -fx-graphic: url("#{$image-path}/HTMLEditor-Background-Color.png"); +} + +.html-editor-cut { + -fx-graphic: url("#{$image-path}/HTMLEditor-Cut.png"); +} + +.html-editor-copy { + -fx-graphic: url("#{$image-path}/HTMLEditor-Copy.png"); +} + +.html-editor-paste { + -fx-graphic: url("#{$image-path}/HTMLEditor-Paste.png"); +} + +.html-editor-align-left { + -fx-graphic: url("#{$image-path}/HTMLEditor-Left.png"); +} + +.html-editor-align-center { + -fx-graphic: url("#{$image-path}/HTMLEditor-Center.png"); +} + +.html-editor-align-right { + -fx-graphic: url("#{$image-path}/HTMLEditor-Right.png"); +} + +.html-editor-align-justify { + -fx-graphic: url("#{$image-path}/HTMLEditor-Justify.png"); +} + +.html-editor-outdent { + -fx-graphic: url("#{$image-path}/HTMLEditor-Outdent.png"); +} + +.html-editor-outdent:dir(rtl) { + -fx-graphic: url("#{$image-path}/HTMLEditor-Outdent-rtl.png"); +} + +.html-editor-indent { + -fx-graphic: url("#{$image-path}/HTMLEditor-Indent.png"); +} + +.html-editor-indent:dir(rtl) { + -fx-graphic: url("#{$image-path}/HTMLEditor-Indent-rtl.png"); +} + +.html-editor-bullets { + -fx-graphic: url("#{$image-path}/HTMLEditor-Bullets.png"); +} + +.html-editor-bullets:dir(rtl) { + -fx-graphic: url("#{$image-path}/HTMLEditor-Bullets-rtl.png"); +} + +.html-editor-numbers { + -fx-graphic: url("#{$image-path}/HTMLEditor-Numbered.png"); +} + +.html-editor-numbers:dir(rtl) { + -fx-graphic: url("#{$image-path}/HTMLEditor-Numbered-rtl.png"); +} + +.html-editor-bold { + -fx-graphic: url("#{$image-path}/HTMLEditor-Bold.png"); +} + +.html-editor-italic { + -fx-graphic: url("#{$image-path}/HTMLEditor-Italic.png"); +} + +.html-editor-underline { + -fx-graphic: url("#{$image-path}/HTMLEditor-Underline.png"); +} + +.html-editor-strike { + -fx-graphic: url("#{$image-path}/HTMLEditor-Strikethrough.png"); +} + +.html-editor-hr { + -fx-graphic: url("#{$image-path}/HTMLEditor-Break.png"); +} \ No newline at end of file diff --git a/styles/src/components/_hyperlink.scss b/styles/src/components/_hyperlink.scss new file mode 100755 index 0000000..f84211d --- /dev/null +++ b/styles/src/components/_hyperlink.scss @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config"as cfg; + +.hyperlink { + -fx-cursor: hand; + -fx-underline: true; + -fx-text-fill: -color-accent-fg; + + &:visited { + -fx-text-fill: -color-fg-default; + } + + &:armed { + -fx-text-fill: -color-fg-default; + -fx-underline: false; + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + + &:show-mnemonics>.mnemonic-underline { + -fx-stroke: -fx-text-fill; + } +} \ No newline at end of file diff --git a/styles/src/components/_index.scss b/styles/src/components/_index.scss new file mode 100755 index 0000000..ded6d1f --- /dev/null +++ b/styles/src/components/_index.scss @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +@use "accordion"; +@use "breadcrumbs"; +@use "button"; +@use "chart"; +@use "checkbox"; +@use "color-picker"; +@use "combo-box"; +@use "custom-text-field"; +@use "data"; +@use "date-picker"; +@use "dialog"; +@use "html-editor"; +@use "hyperlink"; +@use "label"; +@use "menu"; +@use "menu-button"; +@use "pagination"; +@use "popover"; +@use "progress"; +@use "radio"; +@use "scrolling"; +@use "separator"; +@use "slider"; +@use "spinner"; +@use "split-pane"; +@use "tab-pane"; +@use "text-input"; +@use "titled-pane"; +@use "toggle-button"; +@use "toggle-switch"; +@use "toolbar"; +@use "tooltip"; \ No newline at end of file diff --git a/styles/src/components/_label.scss b/styles/src/components/_label.scss new file mode 100755 index 0000000..a7ff83f --- /dev/null +++ b/styles/src/components/_label.scss @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +.label { + -fx-text-fill: -color-fg-default; + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + + &:show-mnemonics>.mnemonic-underline { + -fx-stroke: -color-fg-default; + } +} \ No newline at end of file diff --git a/styles/src/components/_menu-button.scss b/styles/src/components/_menu-button.scss new file mode 100644 index 0000000..74a1551 --- /dev/null +++ b/styles/src/components/_menu-button.scss @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/icons"; +@use "button"; + +$arrow-button-width: 0.5em !default; +$separator-width: 0.75px !default; + +.menu-button, +.split-menu-button { + @include button.base(); + + -fx-padding: 0; + -fx-alignment: CENTER_LEFT; + + >.label { + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + -fx-text-fill: -color-button-fg; + } + + >.arrow-button { + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y 0; + + >.arrow { + @include icons.get("chevron-right", false); + -fx-background-color: -color-button-fg; + -fx-min-width: $arrow-button-width; + } + } + + // "vertically" means popup side (either top or bottom), + // so, it's actually default state + &:openvertically { + >.arrow-button { + >.arrow { + @include icons.get("expand-more", false); + } + } + } + + &:show-mnemonics { + >.label { + >.mnemonic-underline { + -fx-stroke: -color-button-fg; + } + } + } + + &.button-icon { + -fx-padding: 0; + } + + &:hover { + -fx-background-color: -color-button-border-hover, -color-button-bg-hover; + + >.label { + -fx-text-fill: -color-button-fg-hover; + } + + >.arrow-button>.arrow { + -fx-background-color: -color-button-fg-hover; + } + + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-button-fg-hover; + } + } + + &:focused { + -fx-background-color: -color-button-border-focused, -color-button-bg-focused; + + >.label { + -fx-text-fill: -color-button-fg-focused; + } + + >.arrow-button>.arrow { + -fx-background-color: -color-button-fg-focused; + } + + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-button-fg-focused; + } + } + + &:armed, + &:focused:armed { + -fx-background-color: -color-button-border-pressed, -color-button-bg-pressed; + -fx-text-fill: -color-button-fg-pressed; + + >.label { + -fx-text-fill: -color-button-fg-pressed; + } + + >.arrow-button>.arrow { + -fx-background-color: -color-button-fg-pressed; + } + + #{cfg.$font-icon-selector} { + -fx-icon-color: -color-button-fg-pressed; + } + } + + &.flat { + @include button.flat(); + + &:armed, + &:focused:armed { + >.label { + -fx-underline: true; + } + } + } + + &.accent { + @include button.accent(); + } + + &.success { + @include button.success(); + } + + &.danger { + @include button.danger(); + } +} + +.split-menu-button { + + >.label { + -fx-padding: cfg.$padding-y calc(cfg.$padding-x / 2) cfg.$padding-y cfg.$padding-x; + } + + &.button-outlined:hover { + >.arrow-button { + -color-button-fg: -color-fg-emphasis; + } + } + + >.arrow-button { + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-border-color: -color-button-fg; + -fx-border-width: 0 0 0 $separator-width; + -fx-border-insets: calc(cfg.$border-width + 6px) 0 calc(cfg.$border-width + 6px) 0; + } +} \ No newline at end of file diff --git a/styles/src/components/_menu.scss b/styles/src/components/_menu.scss new file mode 100755 index 0000000..c17fe54 --- /dev/null +++ b/styles/src/components/_menu.scss @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/effects"; +@use "../settings/icons"; +@use "../settings/utils"; + +/////////////////////////////////////////////////////////////////////////////// +// MenuBar // +/////////////////////////////////////////////////////////////////////////////// + +$color-menubar-bg: -color-bg-subtle !default; +$color-menubar-bg-hover: utils.saturate($color-menubar-bg, cfg.$darkMode, cfg.$color-delta-hover) !default; +$color-menuitem-bg: -color-bg-default !default; +$color-menuitem-bg-hover: utils.saturate($color-menuitem-bg, cfg.$darkMode, cfg.$color-delta-hover) !default; + +$separator-padding: map-get(cfg.$separators, "small") !default; + +.menu-bar { + + -fx-background-color: -color-border-muted, $color-menubar-bg; + -fx-background-insets: 0 0 0 0, 0 0 1 0; + -fx-background-radius: 0; + -fx-padding: 0; + + >.container { + >.menu-button { + -fx-background-color: transparent; + -fx-background-insets: 0 0 cfg.$border-width 0; + -fx-background-radius: 0; + -fx-padding: cfg.$menu-padding-y cfg.$menu-padding-x cfg.$menu-padding-y cfg.$menu-padding-x; + + // reset padding of menu buttons when in menu bar + >.label { + -fx-padding: 0; + -fx-text-fill: -color-fg-default; + } + + // hide the down arrow for a menu placed in a menubar + >.arrow-button { + -fx-padding: 0; + + >.arrow { + -fx-padding: 0; + -fx-background-color: transparent; + -fx-shape: null; + } + } + + &:hover, + &:focused, + &:showing { + -fx-background-color: $color-menubar-bg-hover, $color-menubar-bg-hover; + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Menu // +/////////////////////////////////////////////////////////////////////////////// + +.menu { + + -fx-background-color: transparent; + + >.right-container { + >.arrow { + @include icons.get("chevron-right", false); + -fx-background-color: -color-fg-muted; + } + } +} + +// vertical (scroll) arrows appear if menu height exceeds container size +.menu-up-arrow { + @include icons.get("arrow-drop-up", true); + -fx-background-color: -color-fg-muted; + -fx-padding: 3px 4px 3px 4px; +} + +.menu-down-arrow { + @include icons.get("arrow-drop-down", true); + -fx-background-color: -color-fg-muted; + -fx-padding: 3px 4px 3px 4px; +} + +/////////////////////////////////////////////////////////////////////////////// +// MenuItem // +/////////////////////////////////////////////////////////////////////////////// + +.menu-item { + -fx-background-color: $color-menuitem-bg; + -fx-padding: cfg.$menu-padding-y cfg.$menu-padding-x cfg.$menu-padding-y cfg.$menu-padding-x; + + >.graphic-container { + -fx-padding: 0 cfg.$graphic-gap 0 0; + } + + // affects both menu and hotkey labels text + >.label { + -fx-padding: 0 1em 0 0; + -fx-text-fill: -color-fg-default; + } + + // left container is for checkbox and radio icons + >.left-container { + -fx-padding: 0 1em 0 0; + } + + // right container is for submenu indication, + // note that hotkey is only displayed when submenu isn't present + >.right-container { + -fx-padding: 0 0 0 0.5em; + } + + &:focused { + -fx-background-color: $color-menuitem-bg-hover, $color-menuitem-bg-hover; + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } +} + +.radio-menu-item:checked>.left-container>.radio, +.check-menu-item:checked>.left-container>.check { + @include icons.get("check", true); + -fx-background-color: -color-fg-muted; + -fx-min-height: cfg.$checkbox-mark-size; + -fx-min-width: cfg.$checkbox-mark-size; + -fx-max-height: cfg.$checkbox-mark-size; + -fx-max-width: cfg.$checkbox-mark-size; +} + +.custom-menu-item.heading { + -fx-padding: cfg.$menu-padding-y cfg.$menu-padding-x cfg.$menu-padding-y cfg.$menu-padding-x; + + &:hover, + &:focused, + &:pressed { + -fx-background-color: transparent; + } + + >.label { + >.text { + -fx-font-weight: bold; + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// PopupMenu // +/////////////////////////////////////////////////////////////////////////////// + +.context-menu { + -fx-background-color: -color-border-muted, $color-menuitem-bg; + -fx-background-insets: 0, 1; + -fx-padding: cfg.$popup-padding-y cfg.$popup-padding-x cfg.$popup-padding-y cfg.$popup-padding-x; + -fx-background-radius: cfg.$border-radius; + @include effects.shadow(-color-bg-inset, cfg.$popup-shadow); + + // no idea what's that thing and how to trigger its appearance + >.scroll-arrow { + -fx-padding: 0.5em; + -fx-background-color: transparent; + + &:hover { + -fx-background-color: $color-menuitem-bg-hover; + -fx-text-fill: -color-fg-default; + } + } + + // use border instead of insets to get thinner line. + .separator:horizontal { + -fx-padding: $separator-padding 0 $separator-padding 0; + + .line { + -fx-border-color: -color-border-muted transparent transparent transparent; + -fx-border-insets: cfg.$border-width 0.5em 0 0.5em; + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Mnemonics // +/////////////////////////////////////////////////////////////////////////////// + +.context-menu:show-mnemonics>.mnemonic-underline, +.menu:show-mnemonics>.mnemonic-underline, +.menu-bar:show-mnemonics>.mnemonic-underline, +.menu-item>.label:show-mnemonics>.mnemonic-underline { + -fx-stroke: -color-fg-default; +} diff --git a/styles/src/components/_pagination.scss b/styles/src/components/_pagination.scss new file mode 100755 index 0000000..eaae417 --- /dev/null +++ b/styles/src/components/_pagination.scss @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/icons"; + +.pagination { + -fx-padding: 0; + -fx-arrow-button-gap: 4; + -fx-arrows-visible: true; + -fx-tooltip-visible: false; + -fx-page-information-visible: true; + -fx-page-information-alignment: bottom; + + >.page { + -fx-background-color: transparent; + } + + >.pagination-control { + -fx-background-color: transparent; + -fx-font-size: 1em; + + >.control-box { + -fx-padding: 2em 0 0 0; + -fx-spacing: 2; + -fx-alignment: CENTER; + + .number-button { + -fx-padding: 0; + } + + >.left-arrow-button { + >.left-arrow { + @include icons.get("arrow-left", false); + -fx-background-color: -color-fg-default; + } + } + + >.right-arrow-button { + >.right-arrow { + @include icons.get("arrow-right", false); + -fx-background-color: -color-fg-default; + } + } + } + + >.page-information { + -fx-padding: 0.5em 0 0 0; + } + } + + // Pagination.STYLE_CLASS_BULLET + &.bullet { + >.pagination-control { + + >.control-box { + -fx-spacing: 0; + + >.left-arrow-button { + -fx-background-radius: 10em; + -fx-padding: 0 0.25em 0 0.083em; // center arrow inside the circle + } + + >.right-arrow-button { + -fx-background-radius: 10em; + -fx-padding: 0 0.083em 0 0.25em; // center arrow inside the circle + } + + >.bullet-button { + -fx-background-radius: 0, 10em, 10em; + -fx-background-color: transparent, -color-border-default, -color-bg-inset; + -fx-background-insets: 0, 5, 6; + + &:selected { + -fx-background-color: transparent, -color-accent-emphasis; + } + } + } + } + } +} \ No newline at end of file diff --git a/styles/src/components/_popover.scss b/styles/src/components/_popover.scss new file mode 100644 index 0000000..718254f --- /dev/null +++ b/styles/src/components/_popover.scss @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/effects"; + +$padding-x: 1em !default; +$padding-y: 1em !default; + +.popover { + -fx-background-color: -color-bg-default; + + >.border { + -fx-stroke: -color-border-default; + -fx-stroke-width: cfg.$border-width; + @include effects.shadow(-color-bg-inset, cfg.$popup-shadow); + } + + >.content { + -fx-padding: $padding-y $padding-x $padding-y $padding-x; + + >.title { + -fx-padding: 0 0 1em 0; + + >.text { + -fx-text-fill: -color-fg-default; + -fx-font-size: cfg.$font-title-4; + -fx-alignment: CENTER_LEFT; + } + + >.icon { + >.graphics { + >.circle { + -fx-fill: transparent; + } + + >.line { + -fx-stroke: -color-fg-default; + -fx-stroke-width: cfg.$border-width; + } + } + } + } + } +} \ No newline at end of file diff --git a/styles/src/components/_progress.scss b/styles/src/components/_progress.scss new file mode 100755 index 0000000..b729a40 --- /dev/null +++ b/styles/src/components/_progress.scss @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/icons"; + +/////////////////////////////////////////////////////////////////////////////// +// ProgressBar // +/////////////////////////////////////////////////////////////////////////////// + +$size: ( + "small": 2px, + "medium": 0.4em, + "large": 0.8em +) !default; + +.progress-bar { + + -fx-indeterminate-bar-length: 60; + -fx-indeterminate-bar-escape: true; + -fx-indeterminate-bar-flip: true; + -fx-indeterminate-bar-animation-time: 2; + + >.track { + -fx-background-color: -color-bg-subtle; + -fx-background-insets: 0; + -fx-background-radius: cfg.$border-radius; + } + + >.bar { + -fx-background-color: -color-accent-emphasis; + -fx-background-insets: 0; + -fx-background-radius: cfg.$border-radius; + -fx-padding: map-get($size, "medium"); + } + + &.small { + >.bar { + -fx-padding: map-get($size, "small"); + } + } + + &.medium { + >.bar { + -fx-padding: map-get($size, "medium"); + } + } + + &.large { + >.bar { + -fx-padding: map-get($size, "large"); + } + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// ProgressIndicator // +/////////////////////////////////////////////////////////////////////////////// + +.progress-indicator { + + -fx-indeterminate-segment-count: 12; + -fx-spin-enabled: true; + + >.determinate-indicator { + >.indicator { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, 1; + } + + >.progress { + -fx-background-color: -color-accent-emphasis; + -fx-padding: 0.6em; // limits tick size + } + + >.tick { + -fx-background-color: -color-fg-emphasis; + -fx-background-insets: 0; + @include icons.get("check"); + } + + >.percentage { + -fx-font-size: cfg.$font-small; + -fx-fill: -color-fg-default; + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + } + + &:indeterminate { + >.spinner { + // undo styling from .spinner + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-border-color: transparent; + -fx-border-width: 0; + -fx-border-radius: 0; + -fx-padding: 0; + } + + .segment { + -fx-background-color: -color-accent-emphasis; + } + + .segment0 { + -fx-shape: "M41.98 14.74 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment1 { + -fx-shape: "M33.75 6.51 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment2 { + -fx-shape: "M22.49 3.5 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment3 { + -fx-shape: "M11.24 6.51 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment4 { + -fx-shape: "M3.01 14.74 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment5 { + -fx-shape: "M0.0 26.0 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment6 { + -fx-shape: "M3.01 37.25 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment7 { + -fx-shape: "M11.25 45.48 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment8 { + -fx-shape: "M22.5 48.5 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment9 { + -fx-shape: "M33.75 45.48 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment10 { + -fx-shape: "M41.98 37.25 a3.5,3.5 0 1,1 0,1 Z"; + } + + .segment11 { + -fx-shape: "M45.0 26.0 a3.5,3.5 0 1,1 0,1 Z"; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_radio.scss b/styles/src/components/_radio.scss new file mode 100755 index 0000000..da44f0a --- /dev/null +++ b/styles/src/components/_radio.scss @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +.radio-button { + + // applied to label + -fx-background-color: -color-bg-default; + -fx-text-fill: -color-fg-default; + -fx-label-padding: cfg.$checkbox-label-padding cfg.$checkbox-label-padding cfg.$checkbox-label-padding cfg.$graphic-gap; + + >.radio { + -fx-background-color: -color-fg-default, -color-bg-default; + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: 1em; // large value to make sure this remains circular + -fx-padding: cfg.$checkbox-mark-padding-y; + -fx-alignment: CENTER; + + >.dot { + -fx-background-color: transparent; + -fx-background-radius: 1em; // large value to make sure this remains circular + -fx-min-height: cfg.$checkbox-mark-size; + -fx-max-height: cfg.$checkbox-mark-size; + -fx-min-width: cfg.$checkbox-mark-size; + -fx-max-width: cfg.$checkbox-mark-size; + } + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + + >.radio { + -fx-opacity: cfg.$opacity-disabled; + } + } + + &:selected { + >.radio { + -fx-background-color: -color-accent-emphasis, -color-bg-default; + + >.dot { + -fx-background-color: -color-accent-emphasis; + } + } + } + + &:show-mnemonics>.mnemonic-underline { + -fx-stroke: -color-fg-default; + } +} \ No newline at end of file diff --git a/styles/src/components/_scrolling.scss b/styles/src/components/_scrolling.scss new file mode 100755 index 0000000..c44533e --- /dev/null +++ b/styles/src/components/_scrolling.scss @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +/////////////////////////////////////////////////////////////////////////////// +// ScrolBar // +/////////////////////////////////////////////////////////////////////////////// + +$scrollbar-thickness: 8px !default; +$thumb-radius: cfg.$border-radius !default; + +.scroll-bar { + + -fx-background-color: cfg.$scrollbar-color-track; + -fx-opacity: cfg.$scrollbar-opacity-inactive; + + >.thumb { + -fx-background-color: cfg.$scrollbar-color-thumb; + -fx-background-radius: $thumb-radius; + } + + >.track { + -fx-background-color: transparent; + -fx-border-radius: 0; + } + + // hide archaic (arguably) increment and decrement buttons + >.increment-button { + visibility: hidden; + -fx-managed: false; + + >.increment-arrow { + -fx-shape: " "; + -fx-padding: 0; + } + } + + >.decrement-button { + visibility: hidden; + -fx-managed: false; + + >.decrement-arrow { + -fx-shape: " "; + -fx-padding: 0; + } + } + + &:horizontal { + -fx-pref-height: $scrollbar-thickness; + } + + &:vertical { + -fx-pref-width: $scrollbar-thickness; + } + + &:hover, + &:pressed, + &:focused { + -fx-opacity: 1; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// ScrollPane // +/////////////////////////////////////////////////////////////////////////////// + +// NOTE: edge-to-edge style class was removed, because it's default now. +// Any control that needs scroll pane with borders should draw them by yourself. +.scroll-pane { + + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-padding: 0; + + >.viewport { + -fx-background-color: transparent; + } + + >.corner { + -fx-background-color: cfg.$scrollbar-color-track; + -fx-opacity: cfg.$scrollbar-opacity-inactive; + } + + &:disabled { + >.scroll-bar { + -fx-opacity: cfg.$scrollbar-opacity-disabled; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_separator.scss b/styles/src/components/_separator.scss new file mode 100755 index 0000000..3615684 --- /dev/null +++ b/styles/src/components/_separator.scss @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +$padding: map-get(cfg.$separators, "medium") !default; +$line-width: cfg.$border-width !default; +$line-color: -color-border-muted !default; + +.separator { + + &:horizontal { + -fx-padding: $padding 0 $padding 0; + + // using border instead of insets to get thinner line + >.line { + -fx-border-color: $line-color transparent transparent transparent; + -fx-border-insets: $line-width 0 0 0; + } + } + + &:vertical { + -fx-padding: 0 $padding 0 $padding; + + >.line { + -fx-border-color: transparent transparent transparent $line-color; + -fx-border-insets: 0 0 0 $line-width; + } + } + + @each $size, $padding in cfg.$separators { + &.#{$size}:horizontal { + -fx-padding: $padding 0 $padding 0; + } + + &.#{$size}:vertical { + -fx-padding: 0 $padding 0 $padding; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_slider.scss b/styles/src/components/_slider.scss new file mode 100755 index 0000000..076e999 --- /dev/null +++ b/styles/src/components/_slider.scss @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "sass:math"; + +$thumb-size: 8px !default; +$thumb-border-width: 2px !default; +$track-size: $thumb-size !default; // visual track height (or width) +$track-margin: 6px !default; // increases clickable track area +$tick-major-size: 5px !default; +$tick-minor-size: 3px !default; + +$_track-padding: math.div($track-size + $track-margin, 2); + +.slider { + + >.thumb { + -fx-background-color: -color-accent-emphasis, -color-bg-default; + -fx-background-insets: 0, 2px; + -fx-background-radius: 50; + -fx-padding: $thumb-size; + } + + >.track { + // transparent background increases clickable track area without increasing visual track height, + // it's also used to center track with thumb + -fx-background-color: transparent, -color-accent-emphasis; + -fx-background-radius: cfg.$border-radius; + } + + // center thumb over track horizontally + &:horizontal { + >.track { + -fx-padding: $_track-padding 0 $_track-padding 0; + -fx-background-insets: 0, $track-margin 0 $track-margin 0; + } + } + + // center thumb over track vertically + &:vertical { + >.track { + -fx-padding: 0 $_track-padding 0 $_track-padding; + -fx-background-insets: 0, 0 $track-margin 0 $track-margin; + } + } + + // there's slightly noticable difference between axis length and track length, + // wontfix this via CSS, because it's probably JavaFX calc problem + >.axis { + -fx-tick-label-fill: -color-fg-muted; + -fx-tick-length: $tick-major-size; + -fx-minor-tick-length: $tick-minor-size; + + >.axis-tick-mark, + >.axis-minor-tick-mark { + -fx-stroke: -color-fg-muted; + } + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } +} \ No newline at end of file diff --git a/styles/src/components/_spinner.scss b/styles/src/components/_spinner.scss new file mode 100755 index 0000000..d972653 --- /dev/null +++ b/styles/src/components/_spinner.scss @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/icons"; +@use "../settings/utils"; + +$button-bg: -color-bg-inset !default; +$button-fg: -color-fg-default !default; +$button-hover: utils.saturate($button-bg, cfg.$darkMode, cfg.$color-delta-hover) !default; +$icon-padding-x: 0.25em !default; + +.spinner { + -fx-background-color: -color-bg-default; + -fx-border-color: -color-border-default; + -fx-border-radius: cfg.$border-radius; + -fx-border-width: cfg.$border-width; + + >.text-field { + -fx-background-radius: cfg.$border-radius 0 0 cfg.$border-radius; + -fx-background-insets: 0; + // align spinner (uses border) and text input height (uses background insets) + -fx-padding: calc(cfg.$padding-y - cfg.$border-width) + calc(cfg.$padding-x - cfg.$border-width) + calc(cfg.$padding-y - cfg.$border-width) + calc(cfg.$padding-x - cfg.$border-width); + } + + >.increment-arrow-button { + -fx-background-color: $button-bg; + -fx-background-insets: 0; + -fx-background-radius: 0 cfg.$border-radius 0 0; + -fx-padding: 10px; + + &:hover { + -fx-background-color: $button-hover; + } + + >.increment-arrow { + -fx-background-color: $button-fg; + -fx-background-insets: 0; + -fx-padding: 0 $icon-padding-x 0 $icon-padding-x; + @include icons.get("arrow-drop-up", false); + } + } + + >.decrement-arrow-button { + -fx-background-color: $button-bg; + -fx-background-insets: -1 0 0 0; + -fx-background-radius: 0 0 cfg.$border-radius 0; + -fx-padding: 10px; + + &:hover { + -fx-background-color: $button-hover; + } + + >.decrement-arrow { + -fx-background-color: $button-fg; + -fx-background-insets: 0; + -fx-padding: 0 $icon-padding-x 0 $icon-padding-x; + @include icons.get("arrow-drop-down", false); + } + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + + &:focused, + &:contains-focus { + &:focused { + -fx-border-color: -color-accent-emphasis; + } + } + + &.arrows-on-left-vertical { + >.text-field { + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-alignment: CENTER_RIGHT; + } + + >.increment-arrow-button { + -fx-background-radius: cfg.$border-radius 0 0 0; + } + + >.decrement-arrow-button { + -fx-background-radius: 0 0 0 cfg.$border-radius; + } + } + + &.arrows-on-right-horizontal { + >.increment-arrow-button { + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-background-insets: 0; + + >.increment-arrow { + @include icons.get("plus", false); + } + } + + >.decrement-arrow-button { + -fx-background-radius: 0; + -fx-background-insets: 0; + + >.decrement-arrow { + @include icons.get("minus", false); + } + } + } + + &.arrows-on-left-horizontal { + >.text-field { + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-alignment: CENTER_RIGHT; + } + + >.increment-arrow-button { + -fx-background-radius: 0; + -fx-background-insets: 0; + + >.increment-arrow { + @include icons.get("plus", false); + } + } + + >.decrement-arrow-button { + -fx-background-radius: cfg.$border-radius 0 0 cfg.$border-radius; + -fx-background-insets: 0; + + >.decrement-arrow { + @include icons.get("minus", false); + } + } + } + + &.split-arrows-horizontal { + >.text-field { + -fx-background-radius: 0; + -fx-alignment: CENTER; + } + + >.increment-arrow-button { + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-background-insets: 0 -1 0 0; + + >.increment-arrow { + @include icons.get("plus", false); + } + } + + >.decrement-arrow-button { + -fx-background-radius: cfg.$border-radius 0 0 cfg.$border-radius; + -fx-background-insets: 0; + + >.decrement-arrow { + @include icons.get("minus", false); + } + } + } + + &.split-arrows-vertical { + >.text-field { + -fx-background-radius: 0; + -fx-alignment: CENTER; + } + + >.increment-arrow-button { + -fx-background-radius: cfg.$border-radius cfg.$border-radius 0 0; + -fx-background-insets: 0; + + >.increment-arrow { + @include icons.get("plus", false); + -fx-padding: $icon-padding-x 0 $icon-padding-x 0; + } + } + + >.decrement-arrow-button { + -fx-background-radius: 0 0 cfg.$border-radius cfg.$border-radius; + -fx-background-insets: 0 0 -1 0; + + >.decrement-arrow { + @include icons.get("minus", false); + -fx-padding: $icon-padding-x 0 $icon-padding-x 0; + } + } + } +} \ No newline at end of file diff --git a/styles/src/components/_split-pane.scss b/styles/src/components/_split-pane.scss new file mode 100755 index 0000000..37f5c02 --- /dev/null +++ b/styles/src/components/_split-pane.scss @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +$divider-thickness: 2px !default; +$grabber-length: 10px !default; + +.split-pane { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-padding: 0; + + >.split-pane-divider { + -fx-background-color: cfg.$scrollbar-color-track; + -fx-padding: 0 $divider-thickness 0 $divider-thickness; + -fx-opacity: cfg.$scrollbar-opacity-inactive; + + // Just for example, this is how we can increase grab area without increasing divider width. + // Sadly, it's only appropriate when split pane items are allowed to have horizontal padding. + //-fx-border-color: color.$background; + //-fx-border-width: 0 5 0 5; + + >.horizontal-grabber { + -fx-background-color: cfg.$scrollbar-color-thumb; + -fx-padding: $grabber-length $divider-thickness $grabber-length $divider-thickness; + } + + >.vertical-grabber { + -fx-background-color: cfg.$scrollbar-color-thumb; + -fx-padding: $divider-thickness $grabber-length $divider-thickness $grabber-length; + } + + &:pressed { + -fx-background-color: -color-accent-emphasis; + + >.horizontal-grabber, + >.vertical-grabber { + -fx-background-color: -color-accent-emphasis; + } + } + + &:hover { + -fx-opacity: 1; + } + + &:disabled { + -fx-opacity: cfg.$scrollbar-opacity-disabled; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_tab-pane.scss b/styles/src/components/_tab-pane.scss new file mode 100755 index 0000000..b3f0827 --- /dev/null +++ b/styles/src/components/_tab-pane.scss @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/effects"; +@use "../settings/icons"; + +$tab-padding-x: cfg.$padding-x !default; +$tab-padding-y: 0.6em !default; +$border-width: 2px !default; + +.tab-pane { + + >.tab-header-area { + -fx-background-insets: 0; + -fx-background-color: -color-bg-default; + -fx-alignment: CENTER; + + >.tab-header-background { + -fx-background-color: transparent; + } + + >.headers-region > .tab { + -fx-padding: $tab-padding-y $tab-padding-x $tab-padding-y $tab-padding-x; + + &:top { + -fx-background-insets: 0 0 0 0, 0 0 $border-width 0; + -fx-background-color: -color-border-default, -color-bg-default; + } + + &:bottom { + -fx-background-insets: 0 0 0 0, 0 0 $border-width 0; + -fx-background-color: -color-border-default, -color-bg-default; + + >.control-buttons-tab > .container > .tab-down-button { + -fx-padding: -0.25em 0.25em 0.25em 0.25em; + } + } + + >.tab-container { + >.tab-label { + -fx-alignment: CENTER; + -fx-text-fill: -color-fg-default; + -fx-padding: 0 5px 0 0; // close button padding + + >* { + -fx-fill: -color-fg-default; + -fx-icon-color: -color-fg-default; + } + } + + >.tab-close-button { + -fx-background-color: -color-fg-default; + -fx-shape: "M 0,0 H1 L 4,3 7,0 H8 V1 L 5,4 8,7 V8 H7 L 4,5 1,8 H0 V7 L 3,4 0,1 Z"; + -fx-scale-shape: false; + + &:hover { + @include effects.highlight(-color-fg-default, 1); + } + } + } + + &:hover { + -fx-background-color: -color-border-default, -color-bg-inset; + } + + &:top:selected, + &:bottom:selected { + -fx-background-color: -color-accent-emphasis, -color-bg-default; + + >.tab-container { + >.tab-label { + -fx-fill: -color-accent-fg; + -fx-text-fill: -color-accent-fg; + + >* { + -fx-fill: -color-accent-fg; + -fx-icon-color: -color-accent-fg; + } + } + + >.tab-close-button { + -fx-background-color: -color-accent-fg; + } + } + } + + // order matters, as JavaFX CSS doesn't support not() selector, + // 'disabled' have to be applied after 'hover' and 'selected' + &:disabled { + -fx-background-color: -color-border-default, -color-bg-default; + + >.tab-container { + -fx-opacity: cfg.$opacity-disabled; + } + } + + // vertical tabs + &:left, + &:right { + -fx-background-insets: 0; + -fx-background-color: transparent; + + &:hover { + -fx-background-color: -color-bg-default; + + >.tab-container { + >.tab-label { + -fx-text-fill: -color-fg-default; + } + + >.tab-close-button { + -fx-background-color: -color-fg-default; + } + } + } + + &:selected { + -fx-background-color: -color-bg-inset; + + >.tab-container { + >.tab-label { + -fx-text-fill: -color-fg-default; + -fx-underline: true; + } + + >.tab-close-button { + -fx-background-color: -color-fg-default; + } + } + } + + // order matters, because JavaFX CSS doesn't support not() selector, + // 'disabled' have to be applied after 'hover' and 'selected' + &:disabled { + -fx-background-color: transparent; + } + } + } + + >.control-buttons-tab > .container { + -fx-padding: 0.25em 0 0 0; + + >.tab-down-button { + -fx-padding: 0.25em 1em 1em 0.25em; + + >.arrow { + @include icons.get("arrow-drop-down", false); + -fx-background-color: -color-fg-default; + } + } + } + } + + // TabPane.STYLE_CLASS_FLOATING + &.floating { + // NOTE: Don't use .floating with vertical tabs + // they are incompatible at the moment + >.tab-header-area { + -fx-background-insets: 0 0 0 0, 0 0 $border-width 0; + -fx-background-color: -color-border-default, -color-bg-default; + } + } +} \ No newline at end of file diff --git a/styles/src/components/_text-input.scss b/styles/src/components/_text-input.scss new file mode 100755 index 0000000..deb6d2d --- /dev/null +++ b/styles/src/components/_text-input.scss @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +.text-input { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: cfg.$border-radius; + -fx-text-fill: -color-fg-default; + + -fx-highlight-fill: -color-accent-subtle; + -fx-highlight-text-fill: -color-fg-default; + -fx-prompt-text-fill: -color-fg-subtle; + + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + -fx-cursor: text; + + &:focused { + -fx-background-color: -color-accent-emphasis, -color-bg-default; + -fx-prompt-text-fill: transparent; + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + + // prevent opacity being applied twice + &>.scroll-pane { + -fx-opacity: 1.0; + } + } + + &:success { + -fx-background-color: -color-success-emphasis, -color-bg-default; + -fx-text-fill: -color-success-fg; + } + + &:danger { + -fx-background-color: -color-danger-emphasis, -color-bg-default; + -fx-text-fill: -color-danger-fg; + } + + // input group + &.left-pill { + -fx-background-radius: cfg.$border-radius 0 0 cfg.$border-radius; + -fx-background-insets: 0, cfg.$border-width 0 cfg.$border-width cfg.$border-width; + + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } + + &.center-pill { + -fx-background-radius: 0; + -fx-background-insets: 0, cfg.$border-width 0 cfg.$border-width 0; + + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } + + &.right-pill { + -fx-background-radius: 0 cfg.$border-radius cfg.$border-radius 0; + -fx-background-insets: 0, cfg.$border-width cfg.$border-width cfg.$border-width 0; + + &:focused { + -fx-background-insets: 0, cfg.$border-width; + } + } +} + +.text-area { + // scroll pane offset to avoid overlapping input borders + -fx-padding: 2px; + -fx-cursor: default; + + .content { + -fx-cursor: text; + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + } +} + +.password-field { + -fx-text-fill: -color-fg-muted; +} \ No newline at end of file diff --git a/styles/src/components/_titled-pane.scss b/styles/src/components/_titled-pane.scss new file mode 100755 index 0000000..c78183e --- /dev/null +++ b/styles/src/components/_titled-pane.scss @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config"as cfg; +@use "../settings/effects"; +@use "../settings/icons"; + +$padding-x: 20px !default; +$padding-y: 10px !default; + +$margin-arrow: 10px !default; +$margin-content: 20px !default; + +$elevation-color: -color-border-subtle !default; +$elevation-interactive: map-get(cfg.$elevation, 2) !default; + +.titled-pane { + -fx-background-color: -color-bg-default; + -fx-text-fill: -color-fg-default; + -fx-effect: none; + + @each $level, + $radius in cfg.$elevation { + &.elevated-#{$level} { + @include effects.elevate($elevation-color, $radius); + } + } + + >.title { + -fx-background-color: -color-border-default, -color-bg-default; + -fx-padding: $padding-y $padding-x $padding-y $padding-x; + + >.text { + -fx-font-size: cfg.$font-title-4; + } + + >.arrow-button { + -fx-background-color: none; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-padding: 0 $margin-arrow 0 0; // distance between icon and text + + >.arrow { + @include icons.get("expand-more", false); + -fx-background-color: -color-fg-default; + -fx-padding: 4px 5px 4px 5px; + } + } + } + + >.content { + -fx-border-color: -color-border-default; + -fx-border-width: 0 cfg.$border-width cfg.$border-width cfg.$border-width; + -fx-border-radius: 0 0 cfg.$border-radius cfg.$border-radius; + -fx-background-radius: 0 0 cfg.$border-radius cfg.$border-radius; + + -fx-background-color: -color-bg-default; + -fx-padding: $margin-content $padding-x $padding-y $padding-x; + -fx-alignment: TOP_LEFT; + } + + // if TitledPane is disabled, its elevation should also be removed, + // othserwise background color will be different due -fxopacity and + // -fx-effect applied at the same time + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } + + // When titled pane is animated and being collapsed you may notice that bottom + // pane border appears slightly before than animation has completed. Sadly, we can't + // draw a border around the whole titled pane (best option), because it will lead to + // accordion height calc problem. Instead we have to draw the borders around the '.title' + // and '.content' and then remove one of those adjacent borders. + &:expanded { + >.title { + -fx-background-radius: cfg.$border-radius cfg.$border-radius 0 0; + -fx-background-insets: 0, cfg.$border-width cfg.$border-width 0 cfg.$border-width; + } + } + + &:collapsed { + >.title { + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: cfg.$border-radius; + } + } + + &.interactive:hover { + @include effects.elevate($elevation-color, $elevation-interactive); + } + + &:show-mnemonics>.mnemonic-underline { + -fx-stroke: -color-fg-default; + } +} \ No newline at end of file diff --git a/styles/src/components/_toggle-button.scss b/styles/src/components/_toggle-button.scss new file mode 100644 index 0000000..25525aa --- /dev/null +++ b/styles/src/components/_toggle-button.scss @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config"as cfg; +@use "button"; + +.toggle-button { + @include button.base(); + + -color-button-bg-selected: -color-accent-emphasis; + -color-button-fg-selected: -color-fg-emphasis; + -color-button-border-focused: -color-border-default; + + -fx-padding: cfg.$padding-y cfg.$padding-x cfg.$padding-y cfg.$padding-x; + + &:selected, + &:selected:focused { + -fx-background-color: -color-button-bg-selected; + -fx-text-fill: -color-button-fg-selected; + -fx-background-insets: 0; + + #{cfg.$font-icon-selector} { + -fx-fill: -color-button-fg-selected; + -fx-icon-color: -color-button-fg-selected; + } + } + + &:show-mnemonics:selected { + >.mnemonic-underline { + -fx-stroke: -color-button-fg-selected; + } + } + + &:selected.left-pill:focused { + -fx-background-insets: 0, cfg.$border-width; + } + + &:selected.center-pill:focused { + -fx-background-insets: 0, cfg.$border-width; + } + + &:selected.right-pill:focused { + -fx-background-insets: 0, cfg.$border-width; + } +} \ No newline at end of file diff --git a/styles/src/components/_toggle-switch.scss b/styles/src/components/_toggle-switch.scss new file mode 100644 index 0000000..e4352a1 --- /dev/null +++ b/styles/src/components/_toggle-switch.scss @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; + +.toggle-switch { + -fx-thumb-move-animation-time: 200; + + >.label-container { + >.label { + -fx-font-size: 1em; + -fx-text-fill: -color-fg-default; + -fx-padding: cfg.$checkbox-label-padding cfg.$graphic-gap cfg.$checkbox-label-padding 0; + } + } + + >.thumb { + -fx-background-color:-color-border-default, -color-fg-emphasis; + -fx-background-insets: 0, cfg.$border-width; + -fx-background-radius: 10em; // make sure this remains circular + -fx-padding: 0.7em; + -fx-alignment: CENTER; + -fx-content-display: LEFT; + -fx-opacity: 0.8; + } + + >.thumb-area { + -fx-background-radius: 1em; + -fx-background-color: -color-border-default, -color-bg-inset; + -fx-background-insets: 0, cfg.$border-width; + -fx-padding: 0.8em 1.5em 0.8em 1.5em; + } + + &:selected { + >.thumb { + -fx-opacity: 1; + } + + >.thumb-area { + -fx-background-color: -color-accent-emphasis; + -fx-background-insets: 0; + } + } + + &:disabled { + -fx-opacity: cfg.$opacity-disabled; + } +} \ No newline at end of file diff --git a/styles/src/components/_toolbar.scss b/styles/src/components/_toolbar.scss new file mode 100755 index 0000000..f9b76be --- /dev/null +++ b/styles/src/components/_toolbar.scss @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/icons"; +@use "../settings/utils"; + +$color-bg: -color-bg-subtle !default; +$color-bg-pressed: utils.saturate($color-bg, cfg.$darkMode, cfg.$color-delta-active) !default; + +$padding-x: cfg.$padding-x !default; +$padding-y: 4px !default; +$spacing: 4px !default; +$border-width: cfg.$border-width !default; + +.tool-bar { + -fx-background-color: -color-border-muted, $color-bg; + -fx-background-insets: 0, 0 0 $border-width 0; + -fx-padding: $padding-y $padding-x $padding-y $padding-x; + -fx-spacing: $spacing; + -fx-alignment: CENTER_LEFT; + + >.container { + + >.button, + >.menu-button, + >.split-menu-button { + -color-button-bg: $color-bg; + -fx-background-insets: 0; + } + + .toggle-button { + -color-button-bg: $color-bg; + -color-button-bg-selected: $color-bg-pressed; + -color-button-fg-selected: -color-fg-default; + -fx-background-insets: 0; + } + + >.separator { + -fx-orientation: vertical; + } + } + + >.tool-bar-overflow-button { + -fx-padding: 0 $padding-x 0 $spacing; + + >.arrow { + @include icons.get("arrow-double-right", false); + -fx-background-color: -color-fg-default; + } + } + + &:vertical { + -fx-background-insets: 0, 0 $border-width 0 0; + -fx-padding: $padding-x $padding-y $padding-x $padding-y; + -fx-alignment: TOP_LEFT; + + >.container { + >.separator { + -fx-orientation: horizontal; + } + } + + >.tool-bar-overflow-button { + -fx-padding: $spacing 0 $padding-x 0; + } + + // this rule existed in modena.css, but JavaFX doesn't apply it automatically, + // you supposed to add it manually. + &.right { + -fx-background-insets: 0, 0 0 0 $border-width; + } + } + + // this rule existed in modena.css, but JavaFX doesn't apply it automatically, + // you supposed to add it manually. + &.bottom { + -fx-background-insets: 0, $border-width 0 0 0; + } +} \ No newline at end of file diff --git a/styles/src/components/_tooltip.scss b/styles/src/components/_tooltip.scss new file mode 100755 index 0000000..c56b6e3 --- /dev/null +++ b/styles/src/components/_tooltip.scss @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/effects"; + +$color-bg: -color-neutral-emphasis-plus !default; +$color-fg: -color-fg-emphasis !default; +$color-shadow: -color-bg-inset !default; +$opacity: 0.85 !default; + +$padding-x: cfg.$padding-x !default; +$padding-y: cfg.$padding-y !default; + +.tooltip { + -fx-background-color: $color-bg; + -fx-background-insets: 0; + -fx-text-fill: $color-fg; + -fx-background-radius: cfg.$border-radius; + -fx-padding: $padding-y $padding-x $padding-y $padding-x; + -fx-opacity: $opacity; + @include effects.shadow(-color-bg-inset, 8); +} \ No newline at end of file diff --git a/styles/src/general/_index.scss b/styles/src/general/_index.scss new file mode 100644 index 0000000..bc82653 --- /dev/null +++ b/styles/src/general/_index.scss @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT + +@forward "root"; +@forward "text"; \ No newline at end of file diff --git a/styles/src/general/_root.scss b/styles/src/general/_root.scss new file mode 100755 index 0000000..cddb7da --- /dev/null +++ b/styles/src/general/_root.scss @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config" as cfg; +@use "../settings/palette" as palette; + +.root { + + -color-fg-default: palette.$fg-default; + -color-fg-muted: palette.$fg-muted; + -color-fg-subtle: palette.$fg-subtle; + -color-fg-emphasis: palette.$fg-onEmphasis; + + -color-bg-default: palette.$canvas-default; + -color-bg-subtle: palette.$canvas-subtle; + -color-bg-inset: palette.$canvas-inset; + + -color-border-default: palette.$border-default; + -color-border-muted: palette.$border-muted; + -color-border-subtle: palette.$border-subtle; + + -color-neutral-emphasis-plus: palette.$neutral-emphasisPlus; + -color-neutral-emphasis: palette.$neutral-emphasis; + -color-neutral-muted: palette.$neutral-muted; + -color-neutral-subtle: palette.$neutral-subtle; + + -color-accent-fg: palette.$accent-fg; + -color-accent-emphasis: palette.$accent-emphasis; + -color-accent-muted: palette.$accent-muted; + -color-accent-subtle: palette.$accent-subtle; + + -color-warning-fg: palette.$warning-fg; + -color-warning-emphasis: palette.$warning-emphasis; + -color-warning-muted: palette.$warning-muted; + -color-warning-subtle: palette.$warning-subtle; + + -color-success-fg: palette.$success-fg; + -color-success-emphasis: palette.$success-emphasis; + -color-success-muted: palette.$success-muted; + -color-success-subtle: palette.$success-subtle; + + -color-danger-fg: palette.$danger-fg; + -color-danger-emphasis: palette.$danger-emphasis; + -color-danger-muted: palette.$danger-muted; + -color-danger-subtle: palette.$danger-subtle; + + -color-chart-1: palette.$chart-1; + -color-chart-2: palette.$chart-2; + -color-chart-3: palette.$chart-3; + -color-chart-4: palette.$chart-4; + -color-chart-5: palette.$chart-5; + -color-chart-6: palette.$chart-6; + -color-chart-7: palette.$chart-7; + -color-chart-8: palette.$chart-8; + + -color-chart-1-alpha70: palette.$chart-1-alpha70; + -color-chart-2-alpha70: palette.$chart-2-alpha70; + -color-chart-3-alpha70: palette.$chart-3-alpha70; + -color-chart-4-alpha70: palette.$chart-4-alpha70; + -color-chart-5-alpha70: palette.$chart-5-alpha70; + -color-chart-6-alpha70: palette.$chart-6-alpha70; + -color-chart-7-alpha70: palette.$chart-7-alpha70; + -color-chart-8-alpha70: palette.$chart-8-alpha70; + + -color-chart-1-alpha20: palette.$chart-1-alpha20; + -color-chart-2-alpha20: palette.$chart-2-alpha20; + -color-chart-3-alpha20: palette.$chart-3-alpha20; + -color-chart-4-alpha20: palette.$chart-4-alpha20; + -color-chart-5-alpha20: palette.$chart-5-alpha20; + -color-chart-6-alpha20: palette.$chart-6-alpha20; + -color-chart-7-alpha20: palette.$chart-7-alpha20; + -color-chart-8-alpha20: palette.$chart-8-alpha20; + + // default props inherited by all nodes + -fx-background-color: -color-bg-default; + -fx-font-family: "Inter"; + -fx-font-size: cfg.$font-default; + + // these are needed for Popup + -fx-background-radius: inherit; + -fx-background-insets: inherit; + -fx-padding: inherit; + + // make popups transparent + &.popup { + -fx-background-color: transparent; + } +} + +// font icons +.ikonli-font-icon { + -fx-icon-color: -color-fg-default; + -fx-icon-size: cfg.$icon-size; +} + +// Hide mnemonic stroke by default. It can only appear when user holds +// Meta button, which adds ":show-mnemonics" pseudo-class to each control +// that set mnemonicParsing = true. +.mnemonic-underline { + -fx-stroke: transparent; +} \ No newline at end of file diff --git a/styles/src/general/_text.scss b/styles/src/general/_text.scss new file mode 100755 index 0000000..ba44a21 --- /dev/null +++ b/styles/src/general/_text.scss @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT + +@use "../settings/config"as cfg; + +// text nodes +.text { + // enable LCD text rendering + -fx-font-smoothing-type: lcd; + + // adjust text alignment within the bounds of text nodes, + // so that the text is always vertically centered within the bounds + -fx-bounds-type: logical_vertical_center; +} + +// default text color +Text { + -fx-fill: -color-fg-default; +} + +// font size +.title-1 { + -fx-font-size: cfg.$font-title-1; + -fx-font-weight: bolder; +} +.title-2 { + -fx-font-size: cfg.$font-title-2; + -fx-font-weight: bolder; +} +.title-3 { + -fx-font-size: cfg.$font-title-3; + -fx-font-weight: bolder; +} +.title-4 { + -fx-font-size: cfg.$font-title-4; + -fx-font-weight: normal; +} +.text-caption { + -fx-font-size: 1em; + -fx-font-weight: bold; +} +.text-small { + -fx-font-size: cfg.$font-small; +} + +// text color +.text.accent { + -fx-fill: -color-accent-fg; +} +.label.accent { + -fx-text-fill: -color-accent-fg; +} +.text.success { + -fx-fill: -color-success-fg; +} +.label.success { + -fx-text-fill: -color-success-fg; +} +.text.warning { + -fx-fill: -color-warning-fg; +} +.label.warning { + -fx-text-fill: -color-warning-fg; +} +.text.danger { + -fx-fill: -color-danger-fg; +} +.label.danger { + -fx-text-fill: -color-danger-fg; +} + +// font weight +// JavaFX CSS parser recognizes all values, but JavaFX engine +// only supports normal and bold. +.text-bold { + -fx-font-weight: bold; +} +.text-bolder { + -fx-font-weight: bolder; +} +.text-normal { + -fx-font-weight: normal; +} +.text-lighter { + -fx-font-weight: lighter; +} + +// font style +.text-italic { + -fx-font-style: italic; +} +.text-oblique { + -fx-font-style: oblique; +} +// only applied to javafx.scene.Text +.text-underlined { + -fx-underline: true; +} +.text-strikethrough { + -fx-strikethrough: true; +} diff --git a/styles/src/primer-dark.scss b/styles/src/primer-dark.scss new file mode 100755 index 0000000..c1204bf --- /dev/null +++ b/styles/src/primer-dark.scss @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +@forward "settings/palette" with ( + + $fg-default: #c9d1d9, + $fg-muted: #8b949e, + $fg-subtle: #6e7681, + $fg-onEmphasis: #ffffff, + + $canvas-default: #0d1117, + $canvas-overlay: #161b22, + $canvas-inset: #010409, + $canvas-subtle: #161b22, + + $border-default: #30363d, + $border-muted: #21262d, + $border-subtle: rgba(240, 246, 252, 0.1), + + $neutral-emphasisPlus: #6e7681, + $neutral-emphasis: #6e7681, + $neutral-muted: rgba(110, 118, 129, 0.4), + $neutral-subtle: rgba(110, 118, 129, 0.1), + + $accent-fg: #58a6ff, + $accent-emphasis: #1f6feb, + $accent-muted: rgba(56, 139, 253, 0.4), + $accent-subtle: rgba(56, 139, 253, 0.15), + + $success-fg: #3fb950, + $success-emphasis: #238636, + $success-muted: rgba(46, 160, 67, 0.4), + $success-subtle: rgba(46, 160, 67, 0.15), + + $warning-fg: #d29922, + $warning-emphasis: #9e6a03, + $warning-muted: rgba(187, 128, 9, 0.4), + $warning-subtle: rgba(187, 128, 9, 0.15), + + $danger-fg: #f85149, + $danger-emphasis: #da3633, + $danger-muted: rgba(248, 81, 73, 0.4), + $danger-subtle: rgba(248, 81, 73, 0.15) +); + +@forward "settings/config" with ( + $darkMode: true, + $color-delta-hover: 15%, + $color-delta-active: 20% +); + +@forward "components/titled-pane" as titled-pane-* with ( + $elevation-color: -color-bg-inset +); + +@forward "components/tooltip" as tooltip-* with ( + $color-bg: -color-bg-inset, + $color-fg: -color-fg-default +); + +@use "general"; +@use "components"; diff --git a/styles/src/primer-light.scss b/styles/src/primer-light.scss new file mode 100755 index 0000000..2fecc3b --- /dev/null +++ b/styles/src/primer-light.scss @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +// Any module variable can be customized at compile time. +// Note that SASS is only loading any module just once so customization order does matter. +// E.g. if A module uses B and B uses C then we should override C variables first, then B, +// then A. Otherwise there will be an exception that we are attempting to change a variable +// in a module that has been already loaded. +// +// Color customization. +// @forward "settings/palette" with ( +// ... +// ); +// +// Shared property customization. +// @forward "settings/config" with ( +// ... +// ); +// +// Individual component property customization. +// @forward "components/split-pane" with ( +// ... +// ); + +@use "general"; +@use "components"; \ No newline at end of file diff --git a/styles/src/settings/_config.scss b/styles/src/settings/_config.scss new file mode 100644 index 0000000..9649826 --- /dev/null +++ b/styles/src/settings/_config.scss @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +$darkMode: false !default; +$color-delta-hover: 5% !default; +$color-delta-active: 10% !default; + +$font-default: 14px !default; // ~= 11pt +$font-family-serif: serif !default; +$font-family-sans-serif: sans-serif !default; +$font-family-monospace: monospace !default; +$font-title-1: 2em !default; +$font-title-2: 1.75em !default; +$font-title-3: 1.5em !default; +$font-title-4: 1.25em !default; +$font-small: 0.8em !default; + +$padding-x: 12px !default; +$padding-y: 8px !default; +$graphic-gap: 6px !default; + +// Most components use background insets to draw its borders due to +// performance reasons. On border radius > 2px corners may look rather ugly. +// They're thicker than border width and sometimes a bit blurry. +$border-width: 1px !default; +$border-radius: 4px !default; + +$opacity-disabled: 0.4 !default; + +// javafx-controls doesn't have iconic fonts support, +// this is just a variable that is used across the theme to style font icons +$font-icon-selector: ".font-icon, .ikonli-font-icon"; +$font-icon-selector-immediate: ">.font-icon, >.ikonli-font-icon"; + +// Ikonli doesn't support 'em' +$icon-size: 18px !default; + +// elevation (level : radius) +$elevation: (1: 2px, 2: 8px, 3: 16px, 4: 20px) !default; + +// check box, radio button +$checkbox-mark-size: 0.75em !default; +$checkbox-mark-padding-y: 3px !default; +$checkbox-mark-padding-x: 4px !default; +$checkbox-label-padding: 2px !default; + +// menu +$popup-padding-x: 2px !default; +$popup-padding-y: 2px !default; +$popup-shadow: 2px !default; +$menu-padding-x: $padding-x !default; +$menu-padding-y: $padding-y !default; + +// separators +$separators: ("small": 0.25em, "medium": 0.75em, "large": 1.5em) !default; + +// scroll bars +$scrollbar-color-track: -color-border-subtle !default; +$scrollbar-color-thumb: -color-fg-muted !default; +$scrollbar-opacity-inactive: 0.5 !default; +$scrollbar-opacity-disabled: 0.25 !default; \ No newline at end of file diff --git a/styles/src/settings/_effects.scss b/styles/src/settings/_effects.scss new file mode 100644 index 0000000..3359a48 --- /dev/null +++ b/styles/src/settings/_effects.scss @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +// Adds shadow effect to popup menus. +@mixin shadow($color, $radius) { + -fx-effect: unquote("dropshadow(gaussian, #{$color}, #{$radius}, 0.75, 0, 0)"); +} + +// Adds elevation effect to boxes. +@mixin elevate($color, $radius) { + -fx-effect: unquote("dropshadow(three-pass-box, #{$color}, #{$radius}, 0.5, 2, 2)"); +} + +// Adds hover (or active) effect to icons. +@mixin highlight($color, $radius) { + -fx-effect: unquote("dropshadow(two-pass-box, #{$color}, #{$radius}, 0.125em, 0, 0)"); +} \ No newline at end of file diff --git a/styles/src/settings/_icons.scss b/styles/src/settings/_icons.scss new file mode 100644 index 0000000..0f6e2fd --- /dev/null +++ b/styles/src/settings/_icons.scss @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +$material-icons: ( + "arrow-double-right": "M5.06 5 4 6.06 7.94 10 4 13.94 5.06 15l5-5z M11 5 9.94 6.06 13.88 10l-3.94 3.94L11 15l5-5z", + "arrow-drop-down": "M7 10l5 5 5-5z", + "arrow-drop-up": "M7 14l5-5 5 5z", + "arrow-left": "M14 7l-5 5 5 5V7z", + "arrow-right": "M10 17l5-5-5-5v10z", + "calendar": "M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V10h16v11zm0-13H4V5h16v3z", + "check": "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z", + "chevron-left": "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z", + "chevron-right": "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z", + "expand-less": "M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", + "expand-more": "M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", + "minus": "M 17,13 H 7 v -2 h 10 z", + "more-vert": "M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z", + "plus": "M 18,12.857142 H 12.857142 V 18 H 11.142858 V 12.857142 H 6 v -1.714284 h 5.142858 V 6 h 1.714284 v 5.142858 H 18 Z", +); + +@mixin get($id, $scale: true) { + $shape: map-get($material-icons, $id); + @if $shape== null { @error "Unknown icon ID = #{$id}."; } + + -fx-shape: $shape; + -fx-scale-shape: $scale; +} \ No newline at end of file diff --git a/styles/src/settings/_palette.scss b/styles/src/settings/_palette.scss new file mode 100644 index 0000000..f3f89d8 --- /dev/null +++ b/styles/src/settings/_palette.scss @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +@use "sass:color"; + +// Muted color is used to emulate semi-transparent background +// (see data.scss, RGBA color). + +$fg-default: #24292f !default; +$fg-muted: #57606a !default; +$fg-subtle: #6e7781 !default; +$fg-onEmphasis: #ffffff !default; + +$canvas-default: #ffffff !default; +$canvas-overlay: #ffffff !default; +$canvas-inset: #f6f8fa !default; +$canvas-subtle: #f6f8fa !default; + +$border-default: #d0d7de !default; +$border-muted: hsla(210, 18%, 87%, 1) !default; +$border-subtle: rgba(27, 31, 36, 0.15) !default; + +$neutral-emphasisPlus: #24292f !default; +$neutral-emphasis: #6e7781 !default; +$neutral-muted: rgba(175, 184, 193, 0.2) !default; +$neutral-subtle: rgba(234, 238, 242, 0.5) !default; + +$accent-fg: #0969da !default; +$accent-emphasis: #0969da !default; +$accent-muted: rgba(84, 174, 255, 0.4) !default; +$accent-subtle: #ddf4ff !default; + +$success-fg: #1a7f37 !default; +$success-emphasis: #2da44e !default; +$success-muted: rgba(74, 194, 107, 0.4) !default; +$success-subtle: #dafbe1 !default; + +$warning-fg: #9a6700 !default; +$warning-emphasis: #bf8700 !default; +$warning-muted: rgba(212, 167, 44, 0.4) !default; +$warning-subtle: #fff8c5 !default; + +$danger-fg: #cf222e !default; +$danger-emphasis: #cf222e !default; +$danger-muted: rgba(255, 129, 130, 0.4) !default; +$danger-subtle: #FFEBE9 !default; + +$chart-1: #f3622d !default; +$chart-2: #fba71b !default; +$chart-3: #57b757 !default; +$chart-4: #41a9c9 !default; +$chart-5: #4258c9 !default; +$chart-6: #9a42c8 !default; +$chart-7: #c84164 !default; +$chart-8: #888888 !default; + +$chart-1-alpha70: color.change($chart-1, $alpha: 0.7) !default; +$chart-2-alpha70: color.change($chart-2, $alpha: 0.7) !default; +$chart-3-alpha70: color.change($chart-3, $alpha: 0.7) !default; +$chart-4-alpha70: color.change($chart-4, $alpha: 0.7) !default; +$chart-5-alpha70: color.change($chart-5, $alpha: 0.7) !default; +$chart-6-alpha70: color.change($chart-6, $alpha: 0.7) !default; +$chart-7-alpha70: color.change($chart-7, $alpha: 0.7) !default; +$chart-8-alpha70: color.change($chart-8, $alpha: 0.7) !default; + +$chart-1-alpha20: color.change($chart-1, $alpha: 0.2) !default; +$chart-2-alpha20: color.change($chart-2, $alpha: 0.2) !default; +$chart-3-alpha20: color.change($chart-3, $alpha: 0.2) !default; +$chart-4-alpha20: color.change($chart-4, $alpha: 0.2) !default; +$chart-5-alpha20: color.change($chart-5, $alpha: 0.2) !default; +$chart-6-alpha20: color.change($chart-6, $alpha: 0.2) !default; +$chart-7-alpha20: color.change($chart-7, $alpha: 0.2) !default; +$chart-8-alpha20: color.change($chart-8, $alpha: 0.2) !default; \ No newline at end of file diff --git a/styles/src/settings/_utils.scss b/styles/src/settings/_utils.scss new file mode 100644 index 0000000..ce8761b --- /dev/null +++ b/styles/src/settings/_utils.scss @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +@use "sass:math"; + +// Darkens or lightens specified color depending on the color scheme. +@function saturate($color, $darkMode, $amount) { + @if $darkMode == true { @return darken($color, $darkMode, $amount); } + @return darken($color, $darkMode, $amount); +} + +@function darken($color, $darkMode, $amount) { + @if $darkMode == true { @return unquote("derive(#{$color}, #{$amount})"); } + @return unquote("derive(#{$color}, -#{$amount})"); +} + +@function lighten($color, $darkMode, $amount) { + @if $darkMode == true { @return unquote("derive(#{$color}, -#{$amount})"); } + @return unquote("derive(#{$color}, #{$amount})"); +} + +// Removes the unit of a length. +@function strip($value) { + @if type-of($value) !="number" { + @error "Invalid `#{type-of($value)}` type. Choose a number type instead."; + } + + @else if type-of($value)=="number"and not math.is-unitless($value) { + @return math.div($value, ($value * 0 + 1)); + } + + @return $value; +} + +// Converts px to em. +@function em($pixels, $font-size: 14px) { + @if (unitless($pixels)) { $pixels: $pixels * 1px; } + @if (unitless($font-size)) { $font-size: $font-size * 1px; } + + @return math.div($pixels, $font-size) * 1em; +}