blob: ec3d671407dde68f7134b12b37184c0052f1d487 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
/*
* RelativeColor.java
*
* Created on March 13, 2004, 1:37 PM
*/
package org.netbeans.swing.laf.dark;
import javax.swing.*;
import java.awt.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* COPIED FROM O.N.SWING.PLAF
*
* A color which can be placed into UIDefaults, which is computed from:
* <ul>
* <li>A base color, as defined in a UI spec - this might be the expected value
* for window titlebars, for example </li>
* <li>A target color, as defined in a UI spec, whose color is not the base
* color, but has a relation to it (such as brighter or darker, or
* hue shifted)</li>
* <li>The actual color - which may differ from the base color if the user has
* customized their UI them (for example, changing the color defaults in
* Windows)</li>
* <li>(optional) A color that the result must contrast with sufficiently that
* text will be readable</li>
* </ul>
* When constructing the real value, a color will be generated which has the
* same relationship to the original value as the base color has to the target
* color.
*
* <h2>What this class is for</h2>
* A number of components in NetBeans have colors that should be based on a
* color taken from the desktop theme. Swing provides a mechanism for getting
* these colors, via UIManager, which will supply correct colors based on the
* desktop theme for a variety of operating systems.
* <p>
* But often the color in a UI specification is not the same as, but related to
* the color that should be used. For example, in windows classic, the tabs
* have a gradient based on a light blue color. The color should be related to
* the dark blue color normally used in Windows for window titles. However,
* if the user has set the window titlebar color to red, a reddish color should
* be used.
* <p>
* This class allows you to provide a base value (the default Windows
* titlebar color, hardcoded) and a prototype value (the blue color that should
* be used <i>if</i> the desktop colors are the defaults), and the <i>actual</i>
* value retrieved from the UI. The instance of this class is then dropped
* into <code>UIDefaults</code>; code can simply call
* <code>UIManager.getColor("someColor")</code> and get the right color without
* being cluttered with the details of deriving colors.
*
* <h2>How it does what it does</h2>
* The base and prototype are split into HSB color components. The relationship
* between the base and prototype values in saturation and brightness is then
* computed. This same relationship is then applied to the actual value
* <i>as a function of the divergence between the base and actual values</i>
* such that the more a color diverges, the less the relationship is applied -
* so that, if the base color is dark blue and the prototype color is light
* blue, but the actual color is light yellow, you get light yellow (as opposed
* to pure white, which a naive application of the relationship would get).
*
* <p><strong>Note:</strong> It <strong>is</strong> possible to create cyclic
* references between RelativeColor instances (for example, a RelativeColor
* that has its own key as one of the keys it should fetch). Don't do that.
*
*/
public class RelativeColor implements UIDefaults.LazyValue {
private Color value = null;
private Color fallback = null;
/** Creates a new instance of RelativeColor.
*
* @param base A Color or UIManager key for a color that the target color is related to
* @param target A Color or UIManager key for a color that is what the target color should be if the
* actual color is equal to the base color
* @param actual Either a Color object or a UIManager String key resolvable
* to a color, which represents the
* actual color, which may or may not match the target color
* @param mustContrast Either a Color object or a UIManager String key
* resolvable to a color which must contrast sufficiently with the derived
* color that text will be readable. This parameter may be null; the others
* may not. */
public RelativeColor(Object base, Object target, Object actual, Object mustContrast) {
if (base == null || target == null || actual == null) {
throw new NullPointerException ("Null argument(s): " + base + ','
+ target + ',' + actual + ',' + mustContrast);
}
if (base instanceof String) {
baseColorKey = (String) base;
} else {
baseColor = (Color) base;
}
if (target instanceof String) {
targetColorKey = (String) target;
} else {
targetColor = (Color) target;
}
if (actual instanceof String) {
actualColorKey = (String) actual;
} else {
actualColor = (Color) actual;
}
if (mustContrast != null) {
if (mustContrast instanceof String) {
mustContrastColorKey = (String) mustContrast;
} else {
mustContrastColor = (Color) mustContrast;
}
}
}
/** Creates a new instance of RelativeColor.
*
* @param base A Color that the target color is related to
* @param target A Color that is what the target color should be if the
* actual color is equal to the base color
* @param actual Either a Color object or a UIManager String key resolvable
* to a color, which represents the
* actual color, which may or may not match the target color
* @param mustContrast Either a Color object or a UIManager String key
* resolvable to a color which must contrast sufficiently with the derived
* color that text will be readable
*/
public RelativeColor(Color base, Color target, Object actual) {
this (base, target, actual, null);
}
public void clear() {
value = null;
if (actualColorKey != null) {
actualColor = null;
}
if (targetColorKey != null) {
targetColor = null;
}
if (mustContrastColorKey != null) {
mustContrastColor = null;
}
if (baseColorKey != null) {
baseColor = null;
}
}
public Object createValue(UIDefaults table) {
if (value != null) {
return value;
}
Color actual = getActualColor();
if( null == actual ) {
Logger.getLogger(RelativeColor.class.getName()).log(Level.INFO, "'actual' color not available");
return Color.gray;
}
Color base = getBaseColor();
if( null == base ) {
Logger.getLogger(RelativeColor.class.getName()).log(Level.INFO, "'base' color not available");
return Color.gray;
}
if (actual.equals(base)) {
value = getTargetColor();
} else {
value = deriveColor (base, actual, getTargetColor());
}
if (hasMustContrastColor()) {
value = ensureContrast(value, getMustContrastColor());
}
return value;
}
/** Convenience getter, as this class is reasonably useful for creating
* derived colors without putting them into UIDefaults */
public Color getColor() {
return (Color) createValue(null);
}
private Color targetColor = null;
private String targetColorKey = null;
private Color getTargetColor() {
if (checkState (targetColor, targetColorKey)) {
targetColor = fetchColor(targetColorKey);
}
return targetColor;
}
private Color baseColor = null;
private String baseColorKey = null;
private Color getBaseColor() {
if (checkState (baseColor, baseColorKey)) {
baseColor = fetchColor(baseColorKey);
}
return baseColor;
}
private Color mustContrastColor = null;
private String mustContrastColorKey = null;
private Color getMustContrastColor() {
if (checkState (mustContrastColor, mustContrastColorKey)) {
mustContrastColor = fetchColor(mustContrastColorKey);
}
return mustContrastColor;
}
private Color actualColor = null;
private String actualColorKey = null;
private Color getActualColor() {
if (checkState (actualColor, actualColorKey)) {
actualColor = fetchColor(actualColorKey);
}
return actualColor;
}
private boolean hasMustContrastColor() {
return mustContrastColor != null || mustContrastColorKey != null;
}
/** Ensures that the key and color are not null, and returns true if the
* color needs to be loaded. */
private boolean checkState(Color color, String key) {
if (color == null && key == null) {
throw new NullPointerException("Both color and key are null for " +
this);
}
return color == null;
}
private Color fetchColor(String key) {
//Todo - check for cyclic references
Color result = UIManager.getColor(key);
if (result == null) {
result = fallback;
}
return result;
}
/** Does the actual leg-work of deriving the color */
static Color deriveColor (Color base, Color actual, Color target) {
float[] baseHSB = Color.RGBtoHSB(base.getRed(), base.getGreen(),
base.getBlue(), null);
float[] targHSB = Color.RGBtoHSB(target.getRed(), target.getGreen(),
target.getBlue(), null);
float[] actualHSB = Color.RGBtoHSB(actual.getRed(), actual.getGreen(),
actual.getBlue(), null);
float[] resultHSB = new float[3];
float[] finalHSB = new float[3];
float[] diff = percentageDiff (actualHSB, baseHSB);
resultHSB[0] = actualHSB[0] + (diff[0] * (targHSB[0] - baseHSB[0]));
resultHSB[1] = actualHSB[1] + (diff[1] * (targHSB[1] - baseHSB[1]));
resultHSB[2] = actualHSB[2] + (diff[2] * (targHSB[2] - baseHSB[2]));
finalHSB[0] = saturate (resultHSB[0]);
finalHSB[1] = saturate (resultHSB[1]);
finalHSB[2] = saturate (resultHSB[2]);
//If the target had *some* color, so should our result - if it pretty
//much doesn't, redistribute some of the brightness to the saturation value
if (targHSB[1] > 0.1 && resultHSB[1] <= 0.1) {
resultHSB[1] = resultHSB[2] * 0.25f;
resultHSB[2] = resultHSB[2] - (resultHSB[2] * 0.25f);
}
Color result = new Color (Color.HSBtoRGB(finalHSB[0], finalHSB[1], finalHSB[2]));
return result;
}
private static float[] percentageDiff (float[] a, float[] b) {
float[] result = new float[3];
for (int i=0; i < 3; i++) {
result[i] = 1 - Math.abs(a[i] - b[i]);
if (result[i] == 0) {
result[i] = 1- a[i];
}
}
return result;
}
private static final void out (String nm, float[] f) {
//XXX for debugging - deleteme
StringBuffer sb = new StringBuffer(nm);
sb.append(": ");
for (int i=0; i < f.length; i++) {
sb.append (Math.round(f[i] * 100));
if (i != f.length-1) {
sb.append(',');
sb.append(' ');
}
}
System.err.println(sb.toString());
}
/** Saturate a float value, clamping values below 0 to 0 and above 1 to 1 */
private static float saturate (float f) {
return Math.max(0, Math.min(1, f));
}
static Color ensureContrast (Color target, Color contrast) {
//XXX - this needs some work. What it should really do:
//Determine the distance from 0.5 for brightness and saturation of the contrasting color, to
//determine the direction in which to adjust. Then adjust in that
//direction as a function of the diff between 0.25 and 0.5 of the
//diff between the colors...or something like that. The point is
//there's a danger zone around 0.5 where things that should be
//adjusted away from each other aren't being
float[] contHSB = Color.RGBtoHSB(contrast.getRed(), contrast.getGreen(),
contrast.getBlue(), null);
float[] targHSB = Color.RGBtoHSB(target.getRed(), target.getGreen(),
target.getBlue(), null);
float[] resultHSB = new float[3];
System.arraycopy(targHSB, 0, resultHSB, 0, 3);
float satDiff = Math.abs (targHSB[1] - contHSB[1]);
float briDiff = Math.abs (targHSB[2] - contHSB[2]);
if (targHSB[1] > 0.6 && resultHSB[1] > 0.6 || (briDiff < 0.45f && satDiff < 0.4f)) {
resultHSB[1] /= 3;
// System.err.println("adjusting saturation to " + resultHSB[1] + " from " + targHSB[1]);
satDiff = Math.abs (targHSB[1] - contHSB[1]);
}
if (briDiff < 0.3 || (satDiff < 0.3 && briDiff < 0.5)) {
float dir = 1.5f * (0.5f - contHSB[2]);
resultHSB[2] = saturate (resultHSB[2] + dir);
// System.err.println("adjusting brightness to " + resultHSB[2] + " from " + targHSB[2]);
}
Color result = new Color (Color.HSBtoRGB(resultHSB[0], resultHSB[1], resultHSB[2]));
return result;
}
}