io.github.palexdev.mfxcomponents.theming.base.WithVariants Maven / Gradle / Ivy
Show all versions of materialfx-all Show documentation
/*
* Copyright (C) 2023 Parisi Alessandro - [email protected]
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
*
* MaterialFX is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the License,
* or (at your option) any later version.
*
* MaterialFX 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with MaterialFX. If not, see .
*/
package io.github.palexdev.mfxcomponents.theming.base;
import io.github.palexdev.mfxcomponents.controls.base.MFXStyleable;
import javafx.scene.Node;
import java.util.*;
import java.util.stream.Collectors;
/**
* Simple API for components that have variants of themselves.
*
* A clarification must be made here. Components typically have a number of default style classes, for example
* if they implement {@link MFXStyleable}, and additionally they can have variants that change their properties/features/appearance.
* This API can be used when creating separate classes is not feasible or just useless.
*
* To make this more comprehensible consider this example:
*
* Material Design 3 buttons have five variants: elevated, filled, outlined, text, and tonal filled. Apart from the
* elevated variant that includes the 'shadow elevation mechanism', the other ones do not add/change any feature/behavior.
* So technically they are good candidates for this API, however they are different types of buttons, each with its
* usage/use cases.
*
* Now on the other hand, consider FABs. Floating Action Buttons mainly have two variants: small and large.
* They are the same exact component, what changes is just their sizes, in other words it's not worth defining two
* new classes to represent these variants, it's enough to just define the variants by changing the 'base' style classes.
*
* Another way to see is as follows:
* MFXElevatedButton has '.mfx-button.elevated' as selector, MFXFilledButton has '.mfx-button.filled' (same is true
* for other buttons), notice how the class defining the button style ('.elevated' and '.filled') are different,
* the difference between the two buttons is more explicit.
*
* MFXFab with standard size has '.mfx-button.fab' as selector, and in its small variant has '.mfx-button.fab.small'
* as selector, notice how the difference between the two is way less emphasized, they are the same component even in style,
* just different size.
*
* The cons in using this mechanism is a less 'comfortable' integration with SceneBuilder. Having separate classes
* allows SceneBuilder to detect the variant and add it to the Custom Controls section. This way variants can still be used
* by adding the variant style class in the properties inspector, but the user is required to remember/check which
* are the applicable classes.
*
* Since components may have multiple variants that can even be combined, this API offers two different ways to apply the
* variants:
* - A 'set' method, which is intended to be implemented so that the component style classes are first reset to its base ones
* and only then the variants are added
*
- An 'add' method, which is intended to be implemented so that the variants are added to the style classes already
* set on the component. A recommendation, is to filter the style classes in a {@code LinkedHashSet} to avoid duplicates
* and unwanted behaviors.
*
* Uses the {@link Variant} API.
*/
public interface WithVariants {
/**
* Adds the given variants to the component.
*/
N addVariants(V... variants);
/**
* Clears the component's variants then adds all the provided ones.
*/
N setVariants(V... variants);
/**
* Removes all the given variants from the component.
*/
N removeVariants(V... variants);
/**
* @return a {@link Set} containing all the applied variants, useful for such queries since it is way faster than
* checking the {@link N#getStyleClass()} list
*/
Set getAppliedVariants();
/**
* @return whether the given variant is contained in {@link #getAppliedVariants()}
*/
default boolean isVariantApplied(V variant) {
return getAppliedVariants().contains(variant);
}
/**
* Adds all the given variants to the given node and to the Set returned by {@link #getAppliedVariants()}.
*/
@SafeVarargs
static > N addVariants(N node, V... variants) {
Set appliedVariants = node.getAppliedVariants();
List classes = new ArrayList<>();
for (V v : variants) {
if (appliedVariants.add(v))
classes.add(v.variantStyleClass());
}
node.getStyleClass().addAll(classes);
return node;
}
/**
* First removes all currently applied variants, then adds the new given ones with {@link #addVariants(Node, Variant[])}.
*/
@SafeVarargs
static > N setVariants(N node, V... variants) {
// Remove all previously applied variants
Set appliedVariants = node.getAppliedVariants();
node.getStyleClass().removeAll(appliedVariants.stream()
.map(Variant::variantStyleClass)
.collect(Collectors.toSet())
);
appliedVariants.clear();
// Apply new ones
Collections.addAll(appliedVariants, variants);
node.getStyleClass().addAll(
appliedVariants.stream()
.map(Variant::variantStyleClass)
.toArray(String[]::new)
);
return node;
}
/**
* Removes all the given variants from the given node.
*/
@SafeVarargs
static > N removeVariants(N node, V... variants) {
Set appliedVariants = node.getAppliedVariants();
Set toRemove = new HashSet<>();
for (V v : variants) {
if (appliedVariants.remove(v))
toRemove.add(v.variantStyleClass());
}
node.getStyleClass().removeAll(toRemove);
return node;
}
}