All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.palexdev.mfxcomponents.skins.MFXSegmentedButtonSkin Maven / Gradle / Ivy

There is a newer version: 11.26.8
Show newest version
package io.github.palexdev.mfxcomponents.skins;

import io.github.palexdev.mfxcomponents.behaviors.MFXSegmentedButtonBehavior;
import io.github.palexdev.mfxcomponents.behaviors.MFXSelectableBehaviorBase;
import io.github.palexdev.mfxcomponents.controls.base.MFXSelectable;
import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase;
import io.github.palexdev.mfxcomponents.controls.buttons.MFXSegmentedButton;
import io.github.palexdev.mfxcomponents.theming.enums.PseudoClasses;
import io.github.palexdev.mfxcore.base.beans.Position;
import io.github.palexdev.mfxcore.utils.fx.LayoutUtils;
import io.github.palexdev.mfxresources.base.properties.IconProperty;
import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
import javafx.beans.InvalidationListener;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.control.ContentDisplay;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

/**
 * Default skin used by {@link MFXSegmentedButton}. Extends {@link MFXSkinBase} as this is nothing more than a simple
 * container for the {@link MFXSegmentedButton#getSegments()}.
 * 

* There are a few peculiarities worth explaining though. As shown by the MD3 guidelines, a segmented button is a * rounded container for a bunch of segments that are the actual buttons. When components are organized like this though, * there are issues with the styling. The guidelines show that the container is delimited by a border, and each segment * separated by a vertical line too. There are several ways to implement this with borders. However, the first and * last segments will need different values. The first will have the border radius only on the left, while the last will * have the border radius applied only on the right. No matter the implementation/theming strategy, we need a way to * distinguish between the first segment, the last segment and the others. For this reason we make use of two new * pseudo classes ':first' and ':last', these are automatically applied by {@link #updateFirstLast()}. *

* Another important detail is the width. Following the guidelines, each segment of the button must have the same width. * Once the segment with the max width is found, the total width will be computed as the found value multiplied by the * number of segments. And during layout, every segment will be resized to have that max found width. */ public class MFXSegmentedButtonSkin extends MFXSkinBase { //================================================================================ // Properties //================================================================================ private InvalidationListener segmentsChanged = i -> onSegmentsChanged(); protected WeakReference first; protected WeakReference last; // Specs protected static double MIN_SEGMENT_WIDTH = 48.0; protected static double MIN_HEIGHT = 40.0; //================================================================================ // Constructors //================================================================================ public MFXSegmentedButtonSkin(MFXSegmentedButton button) { super(button); ObservableList segments = button.getSegments(); updateFirstLast(); addListeners(); getChildren().setAll(segments); } //================================================================================ // Methods //================================================================================ private void addListeners() { MFXSegmentedButton btn = getSkinnable(); btn.getSegments().addListener(segmentsChanged); } /** * Updates the children list when {@link MFXSegmentedButton#getSegments()} change. Before doing so, this also calls * {@link #updateFirstLast()}. */ protected void onSegmentsChanged() { ObservableList segments = getSkinnable().getSegments(); updateFirstLast(); getChildren().setAll(segments); } /** * This is responsible for retrieving the first and last segments from {@link MFXSegmentedButton#getSegments()} and * apply the ':first' and ':last' pseudo classes to them respectively. * It's enough to call this only when the segments list change. Also, if the list becomes empty the old segments * will have the relative pseudo class disabled, and if the list contains only one segment then only ':first' will be * applied. */ protected void updateFirstLast() { ObservableList segments = getSkinnable().getSegments(); if (segments.isEmpty()) { Optional.ofNullable(first) .map(Reference::get) .ifPresent(s -> PseudoClasses.FIRST.setOn(s, false)); Optional.ofNullable(last) .map(Reference::get) .ifPresent(s -> PseudoClasses.LAST.setOn(s, false)); return; } MFXSegment first = segments.get(0); if (!first.equals(Optional.ofNullable(this.first).map(Reference::get).orElse(null))) { PseudoClasses.FIRST.setOn(first, true); this.first = new WeakReference<>(first); } int lastIndex = segments.size() - 1; if (lastIndex == 0) return; MFXSegment last = segments.get(lastIndex); if (!last.equals(Optional.ofNullable(this.last).map(Reference::get).orElse(null))) { PseudoClasses.LAST.setOn(last, true); this.last = new WeakReference<>(last); } } //================================================================================ // Overridden Methods //================================================================================ @Override public double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { MFXSegmentedButton button = getSkinnable(); int density = button.getDensity(); double minH = MIN_HEIGHT - (4.0 * density); return Math.max(minH, super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset)); } @Override public double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { ObservableList segments = getSkinnable().getSegments(); double max = segments.stream() .mapToDouble(LayoutUtils::boundWidth) .max() .orElse(0); return leftInset + max * segments.size() + rightInset; } @Override public double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return getSkinnable().prefWidth(height); } @Override public double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { return getSkinnable().prefHeight(width); } @Override protected void layoutChildren(double x, double y, double w, double h) { ObservableList segments = getSkinnable().getSegments(); double max = segments.stream() .mapToDouble(LayoutUtils::boundWidth) .max() .orElse(0); double sx = x; for (MFXSegment segment : segments) { segment.resizeRelocate(sx, 0, max, h); sx += max; } } @Override public void dispose() { MFXSegmentedButton btn = getSkinnable(); btn.getSegments().removeListener(segmentsChanged); segmentsChanged = null; super.dispose(); } //================================================================================ // Internal Classes //================================================================================ /** * This component is the buttons that fill a {@link MFXSegmentedButton}. It's a simple extension of {@link MFXSelectable} * which enforces the usage of {@link MFXFontIcon}s as the button's graphic. As the Material Design 3 guidelines * show, any segment in a segmented button can have an arbitrary icon, however, the moment the segment is selected * the icon should be set to a 'check mark'. This however is not enforced via code, but depends on the theme. *

* Its default style class is '.segment' and uses skins of type {@link MFXSegmentSkin}. */ public static class MFXSegment extends MFXSelectable> { private final IconProperty icon = new IconProperty(new MFXFontIcon()) { @Override public void set(MFXFontIcon newValue) { MFXFontIcon oldValue = get(); if (newValue == null) { if (oldValue != null) { oldValue.setDescription(""); return; } newValue = new MFXFontIcon(); } super.set(newValue); } }; public MFXSegment() { initialize(); } public MFXSegment(String text) { super(text); setIcon(new MFXFontIcon()); initialize(); } public MFXSegment(String text, MFXFontIcon icon) { super(text); setIcon(icon); initialize(); } private void initialize() { graphicProperty().bind(iconProperty()); } @Override protected MFXSkinBase buildSkin() { return new MFXSegmentSkin(this); } @Override public List defaultStyleClasses() { return List.of("segment"); } @Override public Supplier> defaultBehaviorProvider() { return () -> new MFXSelectableBehaviorBase<>(this); } public MFXFontIcon getIcon() { return icon.get(); } public IconProperty iconProperty() { return icon; } public void setIcon(MFXFontIcon icon) { this.icon.set(icon); } } /** * Default skin used by {@link MFXSegment} and simple extension of {@link MFXButtonSkin}. *

* What changes is the layout strategy. According to MD3 guidelines, a segment's label (text + icon) is always centered. */ public static class MFXSegmentSkin extends MFXButtonSkin> { public MFXSegmentSkin(MFXSegment button) { super(button); } @Override protected void addListeners() {} @Override public double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return Math.max(MIN_SEGMENT_WIDTH, super.computeMinWidth(height, topInset, rightInset, bottomInset, leftInset)); } @Override public double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { MFXSegment segment = getSkinnable(); MFXFontIcon icon = segment.getIcon(); double insets = leftInset + rightInset; double tW = getCachedTextWidth(); if (segment.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) tW = 0; double iW = Math.max(LayoutUtils.boundWidth(icon), icon.getSize()) + segment.getGraphicTextGap(); return insets + tW + iW; } @Override public double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { MFXSegment segment = getSkinnable(); MFXFontIcon icon = segment.getIcon(); double insets = topInset + bottomInset; double iH = Math.max(LayoutUtils.boundHeight(icon), icon.getSize()); double tH = getCachedTextHeight(); return insets + Math.max(iH, tH); } @Override public double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return getSkinnable().prefWidth(height); } @Override public double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { return getSkinnable().prefHeight(width); } @Override protected void layoutChildren(double x, double y, double w, double h) { MFXSegment segment = getSkinnable(); MFXFontIcon icon = segment.getIcon(); double gap = segment.getGraphicTextGap(); if (icon.getDescription() == null || icon.getDescription().isBlank()) { double lW = getCachedTextWidth() + gap; double lH = LayoutUtils.boundHeight(label); label.resize(lW, lH); Position lPos = LayoutUtils.computePosition( segment, label, x, y, w, h, 0, Insets.EMPTY, HPos.CENTER, VPos.CENTER, true, false ); label.relocate(snapPositionX(lPos.getX() - gap / 2), lPos.getY()); } else { layoutInArea(label, x, y, w, h, 0, HPos.CENTER, VPos.CENTER); } surface.resizeRelocate(0, 0, segment.getWidth(), segment.getHeight()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy