org.controlsfx.control.SegmentedBar Maven / Gradle / Ivy
Show all versions of controlsfx Show documentation
/**
* Copyright (c) 2016, 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 org.controlsfx.control;
import com.sun.javafx.css.converters.EnumConverter;
import impl.org.controlsfx.skin.SegmentedBarSkin;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Skin;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* A control that makes it easy to create a horizontal bar that visualizes the
* segmentation of a total value. It consists of several segments, each segment
* representing a value. The sum of all values is the total value of the bar
* (see {@link #totalProperty()}). The bar can be customized by setting a
* factory for the creation of the segment views. Another factory can be set for
* for the creation of info nodes shown by a {@link PopOver}.
*
*
* Example 1:
* The most basic version of the bar. It is using the default segment view
* factory.
*
* SegmentedBar bar = new SegmentedBar();
* bar.getSegments().addAll(
* new Segment(10, "10"),
* new Segment(10, "10"),
* new Segment(10, "10"),
* new Segment(10, "10"),
* new Segment(10, "10"),
* new Segment(50, "50"));
*
* Example 1:
* In this example the bar is used to visualize the usage of disk space for
* various media types (photos, videos, music, ...). A special info node factory
* is supplied to present a useful detailed description of the segment. The
* type "TypeSegment" is a subclass of {@link org.controlsfx.control.SegmentedBar.Segment}
*
* typesBar.setSegmentViewFactory(segment -> new TypeSegmentView(segment));
* typesBar.setInfoNodeFactory(segment -> new InfoLabel(segment.getText() + " " + segment.getValue() + " GB"));
* typesBar.getSegments().addAll(
* new TypeSegment(14, MediaType.PHOTOS),
* new TypeSegment(32, MediaType.VIDEO),
* new TypeSegment(9, MediaType.APPS),
* new TypeSegment(40, MediaType.MUSIC),
* new TypeSegment(5, MediaType.OTHER),
* new TypeSegment(35, MediaType.FREE));
*
*
* @see #setSegmentViewFactory(Callback)
* @see #setInfoNodeFactory(Callback)
*
* @param the segment type
*/
public class SegmentedBar extends ControlsFXControl {
private static final String DEFAULT_STYLE = "segmented-bar";
private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("vertical");
private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("horizontal");
/**
* Constructs a new segmented bar.
*/
public SegmentedBar() {
segments.addListener((Observable it) -> listenToValues());
listenToValues();
getStyleClass().add(DEFAULT_STYLE);
setSegmentViewFactory(segment -> new SegmentView(segment));
setInfoNodeFactory(segment -> {
Label label = new Label("Value: " + segment.getValue());
label.setPadding(new Insets(4));
return label;
});
}
@Override
protected Skin> createDefaultSkin() {
return new SegmentedBarSkin(this);
}
@Override
public String getUserAgentStylesheet() {
return getUserAgentStylesheet(SegmentedBar.class, "segmentedbar.css");
}
// popover node factory
private final ObjectProperty> infoNodeFactory = new SimpleObjectProperty<>(this, "infoNodeFactory");
/**
* Stores a factory callback for creating nodes that will be shown by a popover when the mouse
* enters a segment. The popover / node can provide detailed information about a segment.
*
* @return the info node factory property
*/
public final ObjectProperty> infoNodeFactoryProperty() {
return infoNodeFactory;
}
/**
* Returns the value of {@link #infoNodeFactoryProperty()}.
*
* @return the info node factory
*/
public final Callback getInfoNodeFactory() {
return infoNodeFactory.get();
}
/**
* Sets the value of {@link #infoNodeFactoryProperty()}.
*
* @param factory the info node factory
*/
public void setInfoNodeFactory(Callback factory) {
this.infoNodeFactory.set(factory);
}
// orientation
private ObjectProperty orientation = new StyleableObjectProperty(null) {
@Override
protected void invalidated() {
final boolean vertical = (get() == Orientation.VERTICAL);
pseudoClassStateChanged(VERTICAL_PSEUDOCLASS_STATE,
vertical);
pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE,
!vertical);
}
@Override
public CssMetaData getCssMetaData() {
return StyleableProperties.ORIENTATION;
}
@Override
public Object getBean() {
return SegmentedBar.this;
}
@Override
public String getName() {
return "orientation"; //$NON-NLS-1$
}
};
/**
* Sets the value of the orientation property.
*
* @param value the new orientation (horizontal, vertical).
* @see #orientationProperty()
*/
public final void setOrientation(Orientation value) {
orientationProperty().set(value);
}
/**
* Returns the value of the orientation property.
*
* @return the current orientation of the control
* @see #orientationProperty()
*/
public final Orientation getOrientation() {
return orientation == null ? Orientation.HORIZONTAL : orientation.get();
}
/**
* Returns the styleable object property used for storing the orientation of
* the segmented bar. The CSS property "-fx-orientation" can be used to
* initialize this value.
*
* @return the orientation property
*/
public final ObjectProperty orientationProperty() {
return orientation;
}
private final ObjectProperty> segmentViewFactory = new SimpleObjectProperty<>(this, "segmentViewFactory");
/**
* Stores the segment view factory that is used to create one view for each segment added to
* the control.
*
* @return the property that stores the factory
*/
public final ObjectProperty> segmentViewFactoryProperty() {
return segmentViewFactory;
}
/**
* Returns the value of {@link #segmentViewFactoryProperty()}.
*
* @return the segment view factory
*/
public final Callback getSegmentViewFactory() {
return segmentViewFactory.get();
}
/**
* Sets the value of {@link #segmentViewFactoryProperty()}.
*
* @param factory the segment view factory
*/
public final void setSegmentViewFactory(Callback factory) {
this.segmentViewFactory.set(factory);
}
private final ListProperty segments = new SimpleListProperty<>(this, "segments", FXCollections.observableArrayList());
/**
* A property used to store the list of segments shown by the bar.
*
* @return the segment list
*/
public final ListProperty segmentsProperty() {
return segments;
}
/**
* Returns the list of segments (the model).
*
* @return the list of segments shown by the bar
*/
public final ObservableList getSegments() {
return segments.get();
}
/**
* Sets the list of segments (the model).
*
* @param segments the list of segments shown by the bar
*/
public void setSegments(ObservableList segments) {
this.segments.set(segments);
}
private final ReadOnlyDoubleWrapper total = new ReadOnlyDoubleWrapper(this, "total");
/**
* A read-only property that stores the sum of all segment values attached
* to the bar.
*
* @return the total value of the bar (sum of segments)
*/
public final ReadOnlyDoubleProperty totalProperty() {
return total.getReadOnlyProperty();
}
/**
* Returns the value of {@link #totalProperty()}.
*
* @return the total value of the bar (sum of segments)
*/
public final double getTotal() {
return total.get();
}
private final InvalidationListener sumListener = (Observable it) ->
total.set(segments.stream().collect(Collectors.summingDouble(segment -> segment.getValue())));
private final WeakInvalidationListener weakSumListener = new WeakInvalidationListener(sumListener);
private void listenToValues() {
segments.get().addListener(weakSumListener);
getSegments().forEach(segment -> {
// first remove then add listener to ensure listener is only added once
segment.valueProperty().removeListener(weakSumListener);
segment.valueProperty().addListener(weakSumListener);
});
}
/**
* A model class used by the {@link SegmentedBar} control. Applications
* usually subclass this type for their own specific needs. A segment always
* carries a value and an optional text. Changing the value of the segment
* will trigger a rebuild of the control.
*/
public static class Segment {
/**
* Constructs a new segment with the given value.
*
* @param value the segment's value
*/
public Segment(double value) {
if (value < 0) {
throw new IllegalArgumentException("value must be larger or equal to 0 but was " + value);
}
setValue(value);
}
/**
* Constructs a new segment with the given value.
*
* @param value the segment's value
* @param text the segment's text
*/
public Segment(double value, String text) {
this(value);
setText(text);
}
// text support
private final StringProperty text = new SimpleStringProperty(this, "text");
/**
* Stores the text of the segment (optional).
*
* @return the text property
*/
public final StringProperty textProperty() {
return text;
}
/**
* Sets the value of {@link #textProperty()}.
*
* @param text the new text
*/
public final void setText(String text) {
this.text.set(text);
}
/**
* Returns the value of {@link #textProperty()}.
*
* @return the segment's text
*/
public final String getText() {
return text.get();
}
// value support
private final DoubleProperty value = new SimpleDoubleProperty(this, "value") {
@Override
public void set(double newValue) {
if (newValue < 0) {
throw new IllegalArgumentException("segment value must be >= 0 but was " + newValue);
}
super.set(newValue);
}
};
/**
* Stores the current value of the segment. The value has to be larger or equal
* to zero.
*
* @return the value property
*/
public final DoubleProperty valueProperty() {
return value;
}
/**
* Sets the value of {@link #valueProperty()}.
*
* @param value the new value
*/
public final void setValue(double value) {
this.value.set(value);
}
/**
* Returns the value of {@link #valueProperty()}.
*
* @return the segment's value
*/
public final double getValue() {
return value.get();
}
}
private static class StyleableProperties {
private static final CssMetaData ORIENTATION = new CssMetaData(
"-fx-orientation", new EnumConverter<>( //$NON-NLS-1$
Orientation.class), Orientation.VERTICAL) {
@Override
public Orientation getInitialValue(SegmentedBar node) {
// A vertical bar should remain vertical
return node.getOrientation();
}
@Override
public boolean isSettable(SegmentedBar n) {
return n.orientation == null || !n.orientation.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(
SegmentedBar n) {
return (StyleableProperty) n.orientationProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables = new ArrayList<>(Control.getClassCssMetaData());
styleables.add(ORIENTATION);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* The view class used by the default segment view factory. The view is a simple
* StackPane that contains a label. The default style class is "segment-view".
*
* @see #setSegmentViewFactory(Callback)
*/
public class SegmentView extends StackPane {
private Label label;
/**
* Constructs a new segment view.
*
* @param segment the segment for which the view will be created.
*/
public SegmentView(T segment) {
getStyleClass().add("segment-view");
label = new Label();
label.textProperty().bind(segment.textProperty());
label.setTextOverrun(OverrunStyle.CLIP);
StackPane.setAlignment(label, Pos.CENTER_LEFT);
getChildren().add(label);
}
/**
* The layout method has been overridden to ensure that the label used for displaying the
* text of a segment will be made invisible when there is not enough space to show the entire
* text.
*/
@Override
protected void layoutChildren() {
super.layoutChildren();
label.setVisible(label.prefWidth(-1) < getWidth() - getPadding().getLeft() - getPadding().getRight());
}
}
}