/*
* Copyright (c) 2005, 2013, Oracle and/or its affiliates. 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.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javax.swing.plaf.nimbus;
import javax.swing.Painter;
import javax.swing.JComponent;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.synth.ColorType;
import static javax.swing.plaf.synth.SynthConstants.*;
import javax.swing.plaf.synth.SynthContext;
import javax.swing.plaf.synth.SynthPainter;
import javax.swing.plaf.synth.SynthStyle;
import java.awt.Color;
import java.awt.Font;
import java.awt.Insets;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
*
A SynthStyle implementation used by Nimbus. Each Region that has been
* registered with the NimbusLookAndFeel will have an associated NimbusStyle.
* Third party components that are registered with the NimbusLookAndFeel will
* therefore be handed a NimbusStyle from the look and feel from the
* #getStyle(JComponent, Region) method.
*
*
This class properly reads and retrieves values placed in the UIDefaults
* according to the standard Nimbus naming conventions. It will create and
* retrieve painters, fonts, colors, and other data stored there.
*
*
NimbusStyle also supports the ability to override settings on a per
* component basis. NimbusStyle checks the component's client property map for
* "Nimbus.Overrides". If the value associated with this key is an instance of
* UIDefaults, then the values in that defaults table will override the standard
* Nimbus defaults in UIManager, but for that component instance only.
*
*
Optionally, you may specify the client property
* "Nimbus.Overrides.InheritDefaults". If true, this client property indicates
* that the defaults located in UIManager should first be read, and then
* replaced with defaults located in the component client properties. If false,
* then only the defaults located in the component client property map will
* be used. If not specified, it is assumed to be true.
*
*
You must specify "Nimbus.Overrides" for "Nimbus.Overrides.InheritDefaults"
* to have any effect. "Nimbus.Overrides" indicates whether there are any
* overrides, while "Nimbus.Overrides.InheritDefaults" indicates whether those
* overrides should first be initialized with the defaults from UIManager.
*
*
The NimbusStyle is reloaded whenever a property change event is fired
* for a component for "Nimbus.Overrides" or "Nimbus.Overrides.InheritDefaults".
* So for example, setting a new UIDefaults on a component would cause the
* style to be reloaded.
*
*
The values are only read out of UIManager once, and then cached. If
* you need to read the values again (for example, if the UI is being reloaded),
* then discard this NimbusStyle and read a new one from NimbusLookAndFeel
* using NimbusLookAndFeel.getStyle.
*
*
The primary API of interest in this class for 3rd party component authors
* are the three methods which retrieve painters: #getBackgroundPainter,
* #getForegroundPainter, and #getBorderPainter.
*
*
NimbusStyle allows you to specify custom states, or modify the order of
* states. Synth (and thus Nimbus) has the concept of a "state". For example,
* a JButton might be in the "MOUSE_OVER" state, or the "ENABLED" state, or the
* "DISABLED" state. These are all "standard" states which are defined in synth,
* and which apply to all synth Regions.
*
*
Sometimes, however, you need to have a custom state. For example, you
* want JButton to render differently if it's parent is a JToolbar. In Nimbus,
* you specify these custom states by including a special key in UIDefaults.
* The following UIDefaults entries define three states for this button:
As you can see, the JButton.States entry lists the states
* that the JButton style will support. You then specify the settings for
* each state. If you do not specify the JButton.States entry,
* then the standard Synth states will be assumed. If you specify the entry
* but the list of states is empty or null, then the standard synth states
* will be assumed.
*
* @author Richard Bair
* @author Jasper Potts
*/
public final class NimbusStyle extends SynthStyle {
/* Keys and scales for large/small/mini components, based on Apples sizes */
public static final String LARGE_KEY = "large";
public static final String SMALL_KEY = "small";
public static final String MINI_KEY = "mini";
public static final double LARGE_SCALE = 1.15;
public static final double SMALL_SCALE = 0.857;
public static final double MINI_SCALE = 0.714;
/**
* Special constant used for performance reasons during the get() method.
* If get() runs through all of the search locations and determines that
* there is no value, then NULL will be placed into the values map. This way
* on subsequent lookups it will simply extract NULL, see it, and return
* null rather than continuing the lookup procedure.
*/
private static final Object NULL = '\0';
/**
*
The Color to return from getColorForState if it would otherwise have
* returned null.
*
*
Returning null from getColorForState is a very bad thing, as it causes
* the AWT peer for the component to install a SystemColor, which is not a
* UIResource. As a result, if null is returned from
* getColorForState, then thereafter the color is not updated for other
* states or on LAF changes or updates. This DEFAULT_COLOR is used to
* ensure that a ColorUIResource is always returned from
* getColorForState.
*/
private static final Color DEFAULT_COLOR = new ColorUIResource(Color.BLACK);
/**
* Simple Comparator for ordering the RuntimeStates according to their
* rank.
*/
private static final Comparator STATE_COMPARATOR =
new Comparator() {
@Override
public int compare(RuntimeState a, RuntimeState b) {
return a.state - b.state;
}
};
/**
* The prefix for the component or region that this NimbusStyle
* represents. This prefix is used to lookup state in the UIManager.
* It should be something like Button or Slider.Thumb or "MyButton" or
* ComboBox."ComboBox.arrowButton" or "MyComboBox"."ComboBox.arrowButton"
*/
private String prefix;
/**
* The SynthPainter that will be returned from this NimbusStyle. The
* SynthPainter returned will be a SynthPainterImpl, which will in turn
* delegate back to this NimbusStyle for the proper Painter (not
* SynthPainter) to use for painting the foreground, background, or border.
*/
private SynthPainter painter;
/**
* Data structure containing all of the defaults, insets, states, and other
* values associated with this style. This instance refers to default
* values, and are used when no overrides are discovered in the client
* properties of a component. These values are lazily created on first
* access.
*/
private Values values;
/**
* A temporary CacheKey used to perform lookups. This pattern avoids
* creating useless garbage keys, or concatenating strings, etc.
*/
private CacheKey tmpKey = new CacheKey("", 0);
/**
* Some NimbusStyles are created for a specific component only. In Nimbus,
* this happens whenever the component has as a client property a
* UIDefaults which overrides (or supplements) those defaults found in
* UIManager.
*/
private WeakReference component;
/**
* Create a new NimbusStyle. Only the prefix must be supplied. At the
* appropriate time, installDefaults will be called. At that point, all of
* the state information will be pulled from UIManager and stored locally
* within this style.
*
* @param prefix Something like Button or Slider.Thumb or
* org.jdesktop.swingx.JXStatusBar or ComboBox."ComboBox.arrowButton"
* @param c an optional reference to a component that this NimbusStyle
* should be associated with. This is only used when the component
* has Nimbus overrides registered in its client properties and
* should be null otherwise.
*/
NimbusStyle(String prefix, JComponent c) {
if (c != null) {
this.component = new WeakReference(c);
}
this.prefix = prefix;
this.painter = new SynthPainterImpl(this);
}
/**
* {@inheritDoc}
*
* Overridden to cause this style to populate itself with data from
* UIDefaults, if necessary.
*/
@Override public void installDefaults(SynthContext ctx) {
validate();
//delegate to the superclass to install defaults such as background,
//foreground, font, and opaque onto the swing component.
super.installDefaults(ctx);
}
/**
* Pulls data out of UIDefaults, if it has not done so already, and sets
* up the internal state.
*/
private void validate() {
// a non-null values object is the flag we use to determine whether
// to reparse from UIManager.
if (values != null) return;
// reconstruct this NimbusStyle based on the entries in the UIManager
// and possibly based on any overrides within the component's
// client properties (assuming such a component exists and contains
// any Nimbus.Overrides)
values = new Values();
Map defaults =
((NimbusLookAndFeel) UIManager.getLookAndFeel()).
getDefaultsForPrefix(prefix);
// inspect the client properties for the key "Nimbus.Overrides". If the
// value is an instance of UIDefaults, then these defaults are used
// in place of, or in addition to, the defaults in UIManager.
if (component != null) {
// We know component.get() is non-null here, as if the component
// were GC'ed, we wouldn't be processing its style.
Object o = component.get().getClientProperty("Nimbus.Overrides");
if (o instanceof UIDefaults) {
Object i = component.get().getClientProperty(
"Nimbus.Overrides.InheritDefaults");
boolean inherit = i instanceof Boolean ? (Boolean)i : true;
UIDefaults d = (UIDefaults)o;
TreeMap map = new TreeMap();
for (Object obj : d.keySet()) {
if (obj instanceof String) {
String key = (String)obj;
if (key.startsWith(prefix)) {
map.put(key, d.get(key));
}
}
}
if (inherit) {
defaults.putAll(map);
} else {
defaults = map;
}
}
}
//a list of the different types of states used by this style. This
//list may contain only "standard" states (those defined by Synth),
//or it may contain custom states, or it may contain only "standard"
//states but list them in a non-standard order.
List> states = new ArrayList<>();
//a map of state name to code
Map stateCodes = new HashMap<>();
//This is a list of runtime "state" context objects. These contain
//the values associated with each state.
List runtimeStates = new ArrayList<>();
//determine whether there are any custom states, or custom state
//order. If so, then read all those custom states and define the
//"values" stateTypes to be a non-null array.
//Otherwise, let the "values" stateTypes be null to indicate that
//there are no custom states or custom state ordering
String statesString = (String)defaults.get(prefix + ".States");
if (statesString != null) {
String s[] = statesString.split(",");
for (int i=0; i customState = (State)defaults.get(stateName);
if (customState != null) {
states.add(customState);
}
} else {
states.add(State.getStandardState(s[i]));
}
}
//if there were any states defined, then set the stateTypes array
//to be non-null. Otherwise, leave it null (meaning, use the
//standard synth states).
if (states.size() > 0) {
values.stateTypes = states.toArray(new State>[states.size()]);
}
//assign codes for each of the state types
int code = 1;
for (State> state : states) {
stateCodes.put(state.getName(), code);
code <<= 1;
}
} else {
//since there were no custom states defined, setup the list of
//standard synth states. Note that the "v.stateTypes" is not
//being set here, indicating that at runtime the state selection
//routines should use standard synth states instead of custom
//states. I do need to popuplate this temp list now though, so that
//the remainder of this method will function as expected.
states.add(State.Enabled);
states.add(State.MouseOver);
states.add(State.Pressed);
states.add(State.Disabled);
states.add(State.Focused);
states.add(State.Selected);
states.add(State.Default);
//assign codes for the states
stateCodes.put("Enabled", ENABLED);
stateCodes.put("MouseOver", MOUSE_OVER);
stateCodes.put("Pressed", PRESSED);
stateCodes.put("Disabled", DISABLED);
stateCodes.put("Focused", FOCUSED);
stateCodes.put("Selected", SELECTED);
stateCodes.put("Default", DEFAULT);
}
//Now iterate over all the keys in the defaults table
for (String key : defaults.keySet()) {
//The key is something like JButton.Enabled.backgroundPainter,
//or JButton.States, or JButton.background.
//Remove the "JButton." portion of the key
String temp = key.substring(prefix.length());
//if there is a " or : then we skip it because it is a subregion
//of some kind
if (temp.indexOf('"') != -1 || temp.indexOf(':') != -1) continue;
//remove the separator
temp = temp.substring(1);
//At this point, temp may be any of the following:
//background
//[Enabled].background
//[Enabled+MouseOver].background
//property.foo
//parse out the states and the property
String stateString = null;
String property = null;
int bracketIndex = temp.indexOf(']');
if (bracketIndex < 0) {
//there is not a state string, so property = temp
property = temp;
} else {
stateString = temp.substring(0, bracketIndex);
property = temp.substring(bracketIndex + 2);
}
//now that I have the state (if any) and the property, get the
//value for this property and install it where it belongs
if (stateString == null) {
//there was no state, just a property. Check for the custom
//"contentMargins" property (which is handled specially by
//Synth/Nimbus). Also check for the property being "States",
//in which case it is not a real property and should be ignored.
//otherwise, assume it is a property and install it on the
//values object
if ("contentMargins".equals(property)) {
values.contentMargins = (Insets)defaults.get(key);
} else if ("States".equals(property)) {
//ignore
} else {
values.defaults.put(property, defaults.get(key));
}
} else {
//it is possible that the developer has a malformed UIDefaults
//entry, such that something was specified in the place of
//the State portion of the key but it wasn't a state. In this
//case, skip will be set to true
boolean skip = false;
//this variable keeps track of the int value associated with
//the state. See SynthState for details.
int componentState = 0;
//Multiple states may be specified in the string, such as
//Enabled+MouseOver
String[] stateParts = stateString.split("\\+");
//For each state, we need to find the State object associated
//with it, or skip it if it cannot be found.
for (String s : stateParts) {
if (stateCodes.containsKey(s)) {
componentState |= stateCodes.get(s);
} else {
//Was not a state. Maybe it was a subregion or something
//skip it.
skip = true;
break;
}
}
if (skip) continue;
//find the RuntimeState for this State
RuntimeState rs = null;
for (RuntimeState s : runtimeStates) {
if (s.state == componentState) {
rs = s;
break;
}
}
//couldn't find the runtime state, so create a new one
if (rs == null) {
rs = new RuntimeState(componentState, stateString);
runtimeStates.add(rs);
}
//check for a couple special properties, such as for the
//painters. If these are found, then set the specially on
//the runtime state. Else, it is just a normal property,
//so put it in the UIDefaults associated with that runtime
//state
if ("backgroundPainter".equals(property)) {
rs.backgroundPainter = getPainter(defaults, key);
} else if ("foregroundPainter".equals(property)) {
rs.foregroundPainter = getPainter(defaults, key);
} else if ("borderPainter".equals(property)) {
rs.borderPainter = getPainter(defaults, key);
} else {
rs.defaults.put(property, defaults.get(key));
}
}
}
//now that I've collected all the runtime states, I'll sort them based
//on their integer "state" (see SynthState for how this works).
Collections.sort(runtimeStates, STATE_COMPARATOR);
//finally, set the array of runtime states on the values object
values.states = runtimeStates.toArray(new RuntimeState[runtimeStates.size()]);
}
private Painter