/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed 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.
*/
package com.vaadin.ui.components.colorpicker;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.vaadin.data.HasValue;
import com.vaadin.shared.Registration;
import com.vaadin.shared.ui.MarginInfo;
import com.vaadin.shared.ui.colorpicker.Color;
import com.vaadin.ui.AbstractColorPicker.Coordinates2Color;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Component;
import com.vaadin.ui.HasComponents;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Layout;
import com.vaadin.ui.Slider;
import com.vaadin.ui.Slider.ValueOutOfBoundsException;
import com.vaadin.ui.TabSheet;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;
/**
* A component that represents color selection popup within a color picker.
*
* @since 7.0.0
*/
public class ColorPickerPopup extends Window implements HasValue<Color> {
private static final String STYLENAME = "v-colorpicker-popup";
/** The tabs. */
private final TabSheet tabs = new TabSheet();
private Component rgbTab;
private Component hsvTab;
private Component swatchesTab;
/** The layout. */
private final VerticalLayout layout;
/** The ok button. */
private final Button ok = new Button("OK");
/** The cancel button. */
private final Button cancel = new Button("Cancel");
/** The resize button. */
private final Button resize = new Button("show/hide history");
/** The previously selected color. */
private Color previouslySelectedColor = Color.WHITE;
/** The selected color. */
private Color selectedColor = Color.WHITE;
/** The history. */
private ColorPickerHistory history;
/** The history container. */
private Layout historyContainer;
/** The rgb gradient. */
private ColorPickerGradient rgbGradient;
/** The hsv gradient. */
private ColorPickerGradient hsvGradient;
/** The red slider. */
private Slider redSlider;
/** The green slider. */
private Slider greenSlider;
/** The blue slider. */
private Slider blueSlider;
/** The hue slider. */
private Slider hueSlider;
/** The saturation slider. */
private Slider saturationSlider;
/** The value slider. */
private Slider valueSlider;
/** The preview on the rgb tab. */
private ColorPickerPreview rgbPreview;
/** The preview on the hsv tab. */
private ColorPickerPreview hsvPreview;
/** The preview on the swatches tab. */
private ColorPickerPreview selPreview;
/** The color select. */
private ColorPickerSelect colorSelect;
/** The selectors. */
private final Set<HasValue<Color>> selectors = new HashSet<>();
private boolean readOnly;
private boolean required;
/**
* Set true while the slider values are updated after colorChange. When
* true, valueChange reactions from the sliders are disabled, because
* otherwise the set color may become corrupted as it is repeatedly re-set
* in valueChangeListeners using values from sliders that may not have been
* updated yet.
*/
private boolean updatingColors = false;
private ColorPickerPopup() {
// Set the layout
layout = new VerticalLayout();
layout.setSpacing(false);
layout.setMargin(false);
layout.setWidth("100%");
layout.setHeight(null);
setContent(layout);
setStyleName(STYLENAME);
setResizable(false);
// Create the history
history = new ColorPickerHistory();
history.addValueChangeListener(this::colorChanged);
}
/**
* Instantiates a new color picker popup.
*
* @param initialColor
* the initially selected color
*/
public ColorPickerPopup(Color initialColor) {
this();
selectedColor = initialColor;
initContents();
}
private void initContents() {
// Create the preview on the rgb tab
rgbPreview = new ColorPickerPreview(selectedColor);
rgbPreview.setWidth("240px");
rgbPreview.setHeight("20px");
rgbPreview.addValueChangeListener(this::colorChanged);
selectors.add(rgbPreview);
// Create the preview on the hsv tab
hsvPreview = new ColorPickerPreview(selectedColor);
hsvPreview.setWidth("240px");
hsvPreview.setHeight("20px");
hsvPreview.addValueChangeListener(this::colorChanged);
selectors.add(hsvPreview);
// Create the preview on the swatches tab
selPreview = new ColorPickerPreview(selectedColor);
selPreview.setWidth("100%");
selPreview.setHeight("20px");
selPreview.setRequiredIndicatorVisible(required);
selPreview.addValueChangeListener(this::colorChanged);
selectors.add(selPreview);
// Create the tabs
rgbTab = createRGBTab(selectedColor);
tabs.addTab(rgbTab, "RGB", null);
hsvTab = createHSVTab(selectedColor);
tabs.addTab(hsvTab, "HSV", null);
swatchesTab = createSelectTab();
tabs.addTab(swatchesTab, "Swatches", null);
// Add the tabs
tabs.setWidth("100%");
layout.addComponent(tabs);
// Add the history
history.setWidth("97%");
history.setHeight("22px");
// Create the default colors
List<Color> defaultColors = new ArrayList<>();
defaultColors.add(Color.BLACK);
defaultColors.add(Color.WHITE);
// Create the history
VerticalLayout innerContainer = new VerticalLayout();
innerContainer.setWidth("100%");
innerContainer.setHeight(null);
innerContainer.addComponent(history);
VerticalLayout outerContainer = new VerticalLayout();
outerContainer.setWidth("99%");
outerContainer.setHeight("27px");
outerContainer.addComponent(innerContainer);
historyContainer = outerContainer;
layout.addComponent(historyContainer);
// Add the resize button for the history
resize.addClickListener(this::resizeButtonClick);
resize.setData(false);
resize.setWidth("100%");
resize.setHeight("10px");
resize.setPrimaryStyleName("resize-button");
layout.addComponent(resize);
// Add the buttons
ok.setWidth("70px");
ok.addClickListener(this::okButtonClick);
cancel.setWidth("70px");
cancel.addClickListener(this::cancelButtonClick);
HorizontalLayout buttons = new HorizontalLayout();
buttons.addComponent(ok);
buttons.addComponent(cancel);
buttons.setWidth("100%");
buttons.setHeight("30px");
buttons.setComponentAlignment(ok, Alignment.MIDDLE_CENTER);
buttons.setComponentAlignment(cancel, Alignment.MIDDLE_CENTER);
layout.addComponent(buttons);
}
/**
* Creates the RGB tab.
*
* @return the component
*/
private Component createRGBTab(Color color) {
VerticalLayout rgbLayout = new VerticalLayout();
rgbLayout.setMargin(new MarginInfo(false, false, true, false));
rgbLayout.addComponent(rgbPreview);
rgbLayout.setStyleName("rgbtab");
// Add the RGB color gradient
rgbGradient = new ColorPickerGradient("rgb-gradient", rgbConverter);
rgbGradient.setValue(color);
rgbGradient.addValueChangeListener(this::colorChanged);
rgbLayout.addComponent(rgbGradient);
selectors.add(rgbGradient);
// Add the RGB sliders
VerticalLayout sliders = new VerticalLayout();
sliders.setStyleName("rgb-sliders");
redSlider = createRGBSlider("Red", "red");
greenSlider = createRGBSlider("Green", "green");
blueSlider = createRGBSlider("Blue", "blue");
setRgbSliderValues(color);
redSlider.addValueChangeListener(e -> {
double red = e.getValue();
if (!updatingColors) {
Color newColor = new Color((int) red, selectedColor.getGreen(),
selectedColor.getBlue());
setValue(newColor);
}
});
sliders.addComponent(redSlider);
greenSlider.addValueChangeListener(e -> {
double green = e.getValue();
if (!updatingColors) {
Color newColor = new Color(selectedColor.getRed(), (int) green,
selectedColor.getBlue());
setValue(newColor);
}
});
sliders.addComponent(greenSlider);
blueSlider.addValueChangeListener(e -> {
double blue = e.getValue();
if (!updatingColors) {
Color newColor = new Color(selectedColor.getRed(),
selectedColor.getGreen(), (int) blue);
setValue(newColor);
}
});
sliders.addComponent(blueSlider);
rgbLayout.addComponent(sliders);
return rgbLayout;
}
private Slider createRGBSlider(String caption, String styleName) {
Slider redSlider = new Slider(caption, 0, 255);
redSlider.setStyleName("rgb-slider");
redSlider.setWidth("220px");
redSlider.addStyleName(styleName);
return redSlider;
}
/**
* Creates the hsv tab.
*
* @return the component
*/
private Component createHSVTab(Color color) {
VerticalLayout hsvLayout = new VerticalLayout();
hsvLayout.setMargin(new MarginInfo(false, false, true, false));
hsvLayout.addComponent(hsvPreview);
hsvLayout.setStyleName("hsvtab");
// Add the hsv gradient
hsvGradient = new ColorPickerGradient("hsv-gradient", hsvConverter);
hsvGradient.setValue(color);
hsvGradient.addValueChangeListener(this::colorChanged);
hsvLayout.addComponent(hsvGradient);
selectors.add(hsvGradient);
VerticalLayout sliders = new VerticalLayout();
sliders.setStyleName("hsv-sliders");
hueSlider = new Slider("Hue", 0, 360);
saturationSlider = new Slider("Saturation", 0, 100);
valueSlider = new Slider("Value", 0, 100);
float[] hsv = color.getHSV();
setHsvSliderValues(hsv);
hueSlider.setStyleName("hsv-slider");
hueSlider.addStyleName("hue-slider");
hueSlider.setWidth("220px");
hueSlider.addValueChangeListener(event -> {
if (!updatingColors) {
float hue = Float.parseFloat(event.getValue().toString())
/ 360f;
float saturation = Float.parseFloat(
saturationSlider.getValue().toString()) / 100f;
float value = Float
.parseFloat(valueSlider.getValue().toString()) / 100f;
// Set the color
Color newColor = new Color(
Color.HSVtoRGB(hue, saturation, value));
setValue(newColor);
/*
* Set the background color of the hue gradient. This has to be
* done here since in the conversion the base color information
* is lost when color is black/white
*/
Color bgColor = new Color(Color.HSVtoRGB(hue, 1f, 1f));
hsvGradient.setBackgroundColor(bgColor);
}
});
sliders.addComponent(hueSlider);
saturationSlider.setStyleName("hsv-slider");
saturationSlider.setWidth("220px");
saturationSlider.addValueChangeListener(event -> {
if (!updatingColors) {
float hue = Float.parseFloat(hueSlider.getValue().toString())
/ 360f;
float saturation = Float.parseFloat(event.getValue().toString())
/ 100f;
float value = Float
.parseFloat(valueSlider.getValue().toString()) / 100f;
Color newColor = new Color(
Color.HSVtoRGB(hue, saturation, value));
setValue(newColor);
}
});
sliders.addComponent(saturationSlider);
valueSlider.setStyleName("hsv-slider");
valueSlider.setWidth("220px");
valueSlider.addValueChangeListener(event -> {
if (!updatingColors) {
float hue = Float.parseFloat(hueSlider.getValue().toString())
/ 360f;
float saturation = Float.parseFloat(
saturationSlider.getValue().toString()) / 100f;
float value = Float.parseFloat(event.getValue().toString())
/ 100f;
Color newColor = new Color(
Color.HSVtoRGB(hue, saturation, value));
setValue(newColor);
}
});
sliders.addComponent(valueSlider);
hsvLayout.addComponent(sliders);
return hsvLayout;
}
/**
* Creates the select tab.
*
* @return the component
*/
private Component createSelectTab() {
VerticalLayout selLayout = new VerticalLayout();
selLayout.setMargin(new MarginInfo(false, false, true, false));
selLayout.addComponent(selPreview);
selLayout.addStyleName("seltab");
colorSelect = new ColorPickerSelect();
colorSelect.addValueChangeListener(this::colorChanged);
selLayout.addComponent(colorSelect);
return selLayout;
}
private void resizeButtonClick(ClickEvent event) {
boolean minimize = (Boolean) resize.getData();
if (minimize) {
historyContainer.setHeight("27px");
history.setHeight("22px");
} else {
historyContainer.setHeight("90px");
history.setHeight("85px");
}
resize.setData(!minimize);
}
private void okButtonClick(ClickEvent event) {
fireEvent(new ValueChangeEvent<>(this, previouslySelectedColor, true));
close();
}
private void cancelButtonClick(ClickEvent event) {
close();
}
/**
* Gets the history.
*
* @return the history
*/
public ColorPickerHistory getHistory() {
return history;
}
/**
* Sets the value of this object. If the new value is not equal to
* {@code getValue()}, fires a {@link ValueChangeEvent}. Throws
* {@code NullPointerException} if the value is null.
*
* @param color
* the new value, not {@code null}
* @throws NullPointerException
* if {@code color} is {@code null}
*/
@Override
public void setValue(Color color) {
Objects.requireNonNull(color, "color cannot be null");
previouslySelectedColor = selectedColor;
selectedColor = color;
hsvGradient.setValue(selectedColor);
hsvPreview.setValue(selectedColor);
rgbGradient.setValue(selectedColor);
rgbPreview.setValue(selectedColor);
selPreview.setValue(selectedColor);
}
@Override
public Color getValue() {
return selectedColor;
}
@Override
public Registration addValueChangeListener(
ValueChangeListener<Color> listener) {
Objects.requireNonNull(listener, "listener cannot be null");
return addListener(ValueChangeEvent.class, listener,
ValueChangeListener.VALUE_CHANGE_METHOD);
}
/**
* Gets the color history.
*
* @return the color history
*/
public List<Color> getColorHistory() {
return Collections.unmodifiableList(history.getHistory());
}
private void colorChanged(ValueChangeEvent<Color> event) {
setValue(event.getValue());
updatingColors = true;
setRgbSliderValues(selectedColor);
float[] hsv = selectedColor.getHSV();
setHsvSliderValues(hsv);
updatingColors = false;
for (HasValue<Color> s : selectors) {
if (event.getSource() != s && s != this
&& s.getValue() != selectedColor) {
s.setValue(selectedColor);
}
}
}
private void setRgbSliderValues(Color color) {
try {
redSlider.setValue(((Integer) color.getRed()).doubleValue());
blueSlider.setValue(((Integer) color.getBlue()).doubleValue());
greenSlider.setValue(((Integer) color.getGreen()).doubleValue());
} catch (ValueOutOfBoundsException e) {
getLogger().log(Level.WARNING,
"Unable to set RGB color value to " + color.getRed() + ","
+ color.getGreen() + "," + color.getBlue(),
e);
}
}
private void setHsvSliderValues(float[] hsv) {
try {
hueSlider.setValue(((Float) (hsv[0] * 360f)).doubleValue());
saturationSlider.setValue(((Float) (hsv[1] * 100f)).doubleValue());
valueSlider.setValue(((Float) (hsv[2] * 100f)).doubleValue());
} catch (ValueOutOfBoundsException e) {
getLogger().log(Level.WARNING, "Unable to set HSV color value to "
+ hsv[0] + "," + hsv[1] + "," + hsv[2], e);
}
}
/**
* Checks the visibility of the given tab
*
* @param tab
* The tab to check
* @return true if tab is visible, false otherwise
*/
private boolean isTabVisible(Component tab) {
for (Component child : tabs) {
if (child == tab) {
return true;
}
}
return false;
}
/**
* Checks if tabs are needed and hides them if not
*/
private void checkIfTabsNeeded() {
tabs.setTabsVisible(tabs.getComponentCount() > 1);
}
/**
* Sets the RGB tab visibility.
*
* @param visible
* The visibility of the RGB tab
*/
public void setRGBTabVisible(boolean visible) {
if (visible && !isTabVisible(rgbTab)) {
tabs.addTab(rgbTab, "RGB", null);
checkIfTabsNeeded();
} else if (!visible && isTabVisible(rgbTab)) {
tabs.removeComponent(rgbTab);
checkIfTabsNeeded();
}
}
/**
* Sets the HSV tab visibility.
*
* @param visible
* The visibility of the HSV tab
*/
public void setHSVTabVisible(boolean visible) {
if (visible && !isTabVisible(hsvTab)) {
tabs.addTab(hsvTab, "HSV", null);
checkIfTabsNeeded();
} else if (!visible && isTabVisible(hsvTab)) {
tabs.removeComponent(hsvTab);
checkIfTabsNeeded();
}
}
/**
* Sets the visibility of the Swatches tab.
*
* @param visible
* The visibility of the Swatches tab
*/
public void setSwatchesTabVisible(boolean visible) {
if (visible && !isTabVisible(swatchesTab)) {
tabs.addTab(swatchesTab, "Swatches", null);
checkIfTabsNeeded();
} else if (!visible && isTabVisible(swatchesTab)) {
tabs.removeComponent(swatchesTab);
checkIfTabsNeeded();
}
}
/**
* Sets the visibility of the History.
*
* @param visible
* {@code true} to show the history, {@code false} to hide it
*/
public void setHistoryVisible(boolean visible) {
historyContainer.setVisible(visible);
resize.setVisible(visible);
}
/**
* Sets the preview visibility.
*
* @param visible
* {@code true} to show the preview, {@code false} to hide it
*/
public void setPreviewVisible(boolean visible) {
hsvPreview.setVisible(visible);
rgbPreview.setVisible(visible);
selPreview.setVisible(visible);
}
/** An RGB color converter. */
private Coordinates2Color rgbConverter = new Coordinates2Color() {
@Override
public Color calculate(int x, int y) {
float h = x / 220f;
float s = 1f;
float v = 1f;
if (y < 110) {
s = y / 110f;
} else if (y > 110) {
v = 1f - (y - 110f) / 110f;
}
return new Color(Color.HSVtoRGB(h, s, v));
}
@Override
public int[] calculate(Color color) {
float[] hsv = color.getHSV();
int x = Math.round(hsv[0] * 220f);
int y = 0;
// lower half
if (hsv[1] == 1f) {
y = Math.round(110f - (hsv[1] + hsv[2]) * 110f);
} else {
y = Math.round(hsv[1] * 110f);
}
return new int[] { x, y };
}
};
/** An HSV color converter. */
private Coordinates2Color hsvConverter = new Coordinates2Color() {
@Override
public int[] calculate(Color color) {
float[] hsv = color.getHSV();
// Calculate coordinates
int x = Math.round(hsv[2] * 220.0f);
int y = Math.round(220 - hsv[1] * 220.0f);
// Create background color of clean color
Color bgColor = new Color(Color.HSVtoRGB(hsv[0], 1f, 1f));
hsvGradient.setBackgroundColor(bgColor);
return new int[] { x, y };
}
@Override
public Color calculate(int x, int y) {
float saturation = 1f - y / 220.0f;
float value = x / 220.0f;
float hue = Float.parseFloat(hueSlider.getValue().toString())
/ 360f;
Color color = new Color(Color.HSVtoRGB(hue, saturation, value));
return color;
}
};
@Override
public void setRequiredIndicatorVisible(boolean visible) {
required = visible;
if (selPreview != null) {
selPreview.setRequiredIndicatorVisible(required);
}
}
@Override
public boolean isRequiredIndicatorVisible() {
return required;
}
private static Logger getLogger() {
return Logger.getLogger(ColorPickerPopup.class.getName());
}
@Override
public void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
updateColorComponents();
}
@Override
public boolean isReadOnly() {
return readOnly;
}
private void updateColorComponents() {
if (getContent() != null) {
updateColorComponents(getContent());
}
}
private void updateColorComponents(Component component) {
if (component instanceof HasValue<?>) {
((HasValue<?>) component).setReadOnly(isReadOnly());
((HasValue<?>) component)
.setRequiredIndicatorVisible(isRequiredIndicatorVisible());
}
if (component instanceof HasComponents) {
Iterator<Component> iterator = ((HasComponents) component)
.iterator();
while (iterator.hasNext()) {
updateColorComponents(iterator.next());
}
}
}
}