/*
* Copyright (c) 2002-2015, JIDE Software Inc. 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.
*/
package jidefx.scene.control.field.popup;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import jidefx.scene.control.decoration.DecorationPane;
import jidefx.scene.control.field.FormattedTextField;
import jidefx.scene.control.field.IntegerField;
import jidefx.utils.converter.javafx.HexColorConverter;
import java.util.Locale;
public class ColorPopupContent extends DecorationPane implements PopupContent<Color> {
private static final String STYLE_CLASS_DEFAULT = "popup-content"; //NON-NLS
private static final int PICKER_PADDING = 2;
private static final int RECT_SIZE = 170;
private static final int ARROW_SIZE = 10;
private static final int PICKER_WIDTH = 220;
private static final int PICKER_HEIGHT = 280;
private boolean changeIsLocal = false;
private DoubleProperty hue = new SimpleDoubleProperty() {
@Override
protected void invalidated() {
if (!changeIsLocal) {
changeIsLocal = true;
_colorProperty.set(Color.hsb(hue.get(), clamp(sat.get() / 100), clamp(bright.get() / 100)));
changeIsLocal = false;
}
}
};
private DoubleProperty sat = new SimpleDoubleProperty() {
@Override
protected void invalidated() {
if (!changeIsLocal) {
changeIsLocal = true;
_colorProperty.set(Color.hsb(hue.get(), clamp(sat.get() / 100), clamp(bright.get() / 100)));
changeIsLocal = false;
}
}
};
private DoubleProperty bright = new SimpleDoubleProperty() {
@Override
protected void invalidated() {
if (!changeIsLocal) {
changeIsLocal = true;
_colorProperty.set(Color.hsb(hue.get(), clamp(sat.get() / 100), clamp(bright.get() / 100)));
changeIsLocal = false;
}
}
};
private ObjectProperty<Color> _colorProperty = new SimpleObjectProperty<Color>(Color.RED) {
@Override
protected void invalidated() {
if (!changeIsLocal) {
changeIsLocal = true;
Color c = get();
if (c == null) c = Color.WHITE;
hue.set(c.getHue());
sat.set(c.getSaturation() * 100);
bright.set(c.getBrightness() * 100);
changeIsLocal = false;
}
}
};
@Override
public final ObjectProperty<Color> valueProperty() {
return _colorProperty;
}
@Override
public final Color getValue() {
return _colorProperty.get();
}
@Override
public final void setValue(Color value) {
_colorProperty.set(value);
}
public ColorPopupContent() {
super(new Group());
getStylesheets().add(PopupContent.class.getResource("PopupContent.css").toExternalForm()); //NON-NLS
getStyleClass().add(STYLE_CLASS_DEFAULT);
// create rectangle to capture mouse events to hide
Rectangle windowClickRect = new Rectangle(PICKER_PADDING + PICKER_WIDTH + PICKER_PADDING, PICKER_PADDING + PICKER_HEIGHT + PICKER_PADDING);
windowClickRect.setFill(Color.TRANSPARENT);
Circle colorRectIndicator = new Circle(60, 60, 5);
colorRectIndicator.setStroke(Color.WHITE);
colorRectIndicator.setFill(null);
colorRectIndicator.setEffect(new DropShadow(2, 0, 1, Color.BLACK));
colorRectIndicator.centerXProperty().bind(new DoubleBinding() {
{
bind(sat);
}
@Override
protected double computeValue() {
return (PICKER_PADDING + 10) + (RECT_SIZE * (sat.get() / 100));
}
});
colorRectIndicator.centerYProperty().bind(new DoubleBinding() {
{
bind(bright);
}
@Override
protected double computeValue() {
return (PICKER_PADDING + ARROW_SIZE + 10) + (RECT_SIZE * (1 - (bright.get() / 100)));
}
});
final Rectangle colorRect = new Rectangle(PICKER_PADDING + 10, PICKER_PADDING + ARROW_SIZE + 10, RECT_SIZE, RECT_SIZE);
colorRect.setStroke(Color.GRAY);
colorRect.fillProperty().bind(new ObjectBinding<Paint>() {
{
bind(_colorProperty);
}
@Override
protected Paint computeValue() {
return Color.hsb(hue.getValue(), 1, 1);
}
});
Rectangle colorRectOverlayOne = new Rectangle(PICKER_PADDING + 10, PICKER_PADDING + ARROW_SIZE + 10, RECT_SIZE, RECT_SIZE);
colorRectOverlayOne.setFill(new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, new Stop(0, Color.rgb(255, 255, 255, 1)), new Stop(1, Color.rgb(255, 255, 255, 0))));
EventHandler<MouseEvent> rectMouseHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
double x = event.getX() - colorRect.getX();
double y = event.getY() - colorRect.getY();
sat.set(clamp(x / RECT_SIZE) * 100);
bright.set(100 - (clamp(y / RECT_SIZE) * 100));
}
};
Rectangle colorRectOverlayTwo = new Rectangle(PICKER_PADDING + 10, PICKER_PADDING + ARROW_SIZE + 10, RECT_SIZE, RECT_SIZE);
colorRectOverlayTwo.setFill(new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, new Stop(0, Color.rgb(0, 0, 0, 0)), new Stop(1, Color.rgb(0, 0, 0, 1))));
colorRectOverlayTwo.setOnMouseDragged(rectMouseHandler);
colorRectOverlayTwo.setOnMouseClicked(rectMouseHandler);
final Rectangle colorBar = new Rectangle(PICKER_PADDING + PICKER_WIDTH - 30, PICKER_PADDING + ARROW_SIZE + 10, 20, RECT_SIZE);
colorBar.setStroke(Color.GRAY);
colorBar.setFill(createHueGradient());
Rectangle colorBarIndicator = new Rectangle(PICKER_PADDING + PICKER_WIDTH - 32, PICKER_PADDING + ARROW_SIZE + 15, 24, 10);
colorBarIndicator.setArcWidth(4);
colorBarIndicator.setArcHeight(4);
colorBarIndicator.setStroke(Color.WHITE);
colorBarIndicator.setFill(null);
colorBarIndicator.setEffect(new DropShadow(2, 0, 1, Color.BLACK));
colorBarIndicator.yProperty().bind(new DoubleBinding() {
{
bind(hue);
}
@Override
protected double computeValue() {
return (PICKER_PADDING + ARROW_SIZE + 5) + (RECT_SIZE * (hue.get() / 360));
}
});
EventHandler<MouseEvent> barMouseHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
double y = event.getY() - colorBar.getY();
hue.set(clamp(y / RECT_SIZE) * 360);
}
};
colorBar.setOnMouseDragged(barMouseHandler);
colorBar.setOnMouseClicked(barMouseHandler);
Label brightnessLabel = new Label(getResourceString("brightness"));
brightnessLabel.setMinWidth(Control.USE_PREF_SIZE);
GridPane.setConstraints(brightnessLabel, 0, 0);
Slider brightnessSlider = new Slider();
brightnessSlider.setMin(0);
brightnessSlider.setMax(100);
brightnessSlider.setId("BrightnessSlider"); //NON-NLS
brightnessSlider.valueProperty().bindBidirectional(bright);
GridPane.setConstraints(brightnessSlider, 1, 0);
IntegerField brightnessField = new IntegerField();
brightnessField.setSpinnersVisible(true);
brightnessField.setPrefColumnCount(7);
brightnessField.installAdjustmentMouseHandler(brightnessLabel);
bindBidirectional(brightnessField.valueProperty(), bright);
GridPane.setConstraints(brightnessField, 2, 0);
Label saturationLabel = new Label(getResourceString("saturation"));
saturationLabel.setMinWidth(Control.USE_PREF_SIZE);
GridPane.setConstraints(saturationLabel, 0, 1);
Slider saturationSlider = new Slider();
saturationSlider.setMin(0);
saturationSlider.setMax(100);
saturationSlider.setId("SaturationSlider"); //NON-NLS
saturationSlider.valueProperty().bindBidirectional(sat);
GridPane.setConstraints(saturationSlider, 1, 1);
saturationSlider.styleProperty().bind(new StringBinding() {
{
bind(_colorProperty);
}
@Override
protected String computeValue() {
return "picker-color: hsb(" + hue.get() + ",100%,100%);"; //NON-NLS
}
});
IntegerField saturationField = new IntegerField();
saturationField.setSpinnersVisible(true);
saturationField.setPrefColumnCount(7);
saturationField.setValue(0);
saturationField.installAdjustmentMouseHandler(saturationLabel);
bindBidirectional(saturationField.valueProperty(), sat);
GridPane.setConstraints(saturationField, 2, 1);
Label webLabel = new Label(getResourceString("web"));
webLabel.setMinWidth(Control.USE_PREF_SIZE);
GridPane.setConstraints(webLabel, 0, 2);
FormattedTextField<Color> webField = new FormattedTextField<>();
webField.setStringConverter(new HexColorConverter().toStringConverter());
webField.valueProperty().bindBidirectional(_colorProperty);
webField.setComboBoxLike(false);
webField.setEditable(false);
GridPane.setConstraints(webField, 1, 2, 2, 1);
GridPane controls = new GridPane();
controls.setVgap(5);
controls.setHgap(5);
controls.getChildren().addAll(brightnessLabel, brightnessSlider, brightnessField, saturationLabel, saturationSlider, saturationField, webLabel, webField);
controls.setManaged(false);
controls.resizeRelocate(
PICKER_PADDING + 10,
PICKER_PADDING + ARROW_SIZE + 10 + 170 + 10,
PICKER_WIDTH - 20,
80);
((Group) getContent()).getChildren().addAll(windowClickRect, colorRect, colorRectOverlayOne, colorRectOverlayTwo, colorBar, colorRectIndicator, colorBarIndicator, controls);
}
private void bindBidirectional(ObjectProperty<Integer> objectProperty, DoubleProperty doubleProperty) {
doubleProperty.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
objectProperty.set(newValue.intValue());
}
});
objectProperty.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
doubleProperty.setValue(newValue.doubleValue());
}
});
}
private static double clamp(double value) {
return value < 0 ? 0 : value > 1 ? 1 : value;
}
private static LinearGradient createHueGradient() {
double offset;
Stop[] stops = new Stop[255];
for (int y = 0; y < 255; y++) {
offset = 1 - (1.0 / 255) * y;
int h = (int) ((y / 255.0) * 360);
stops[y] = new Stop(offset, Color.hsb(h, 1.0, 1.0));
}
return new LinearGradient(0f, 1f, 1f, 0f, true, CycleMethod.NO_CYCLE, stops);
}
/**
* Gets the localized string from resource bundle. Subclass can override it to provide its own string. Available
* keys are defined in grid.properties that begins with "Filter." and lucene.properties that begins with "Lucene".
*
* @param key the key to the resource.
* @return the localized string.
*/
public String getResourceString(String key) {
if (key == null) {
return "";
}
return PopupsResource.getResourceBundle(Locale.getDefault()).getString(key);
}
}