/* * Copyright 2005-2006 Sun Microsystems, 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. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.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:
* *
* JButton.States = Enabled, Disabled, Toolbar
* JButton[Enabled].backgroundPainter = somePainter
* JButton[Disabled].background = BLUE
* JButton[Toolbar].backgroundPainter = someOtherPaint
*
*
* 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.
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.
Overridden to cause this style to populate itself with data from * UIDefaults, if necessary.
* *In addition, NimbusStyle handles ColorTypes slightly differently from * Synth.
*Overridden to cause this style to populate itself with data from * UIDefaults, if necessary.
* *Properties in UIDefaults may be specified in a chained manner. For * example: *
* background * Button.opacity * Button.Enabled.foreground * Button.Enabled+Selected.background ** *
In this example, suppose you were in the Enabled+Selected state and * searched for "foreground". In this case, we first check for * Button.Enabled+Selected.foreground, but no such color exists. We then * fall back to the next valid state, in this case, * Button.Enabled.foreground, and have a match. So we return it.
* *Again, if we were in the state Enabled and looked for "background", we * wouldn't find it in Button.Enabled, or in Button, but would at the top * level in UIManager. So we return that value.
* *One special note: the "key" passed to this method could be of the form * "background" or "Button.background" where "Button" equals the prefix * passed to the NimbusStyle constructor. In either case, it looks for * "background".
* * @param ctx * @param key must not be null */ @Override public Object get(SynthContext ctx, Object key) { Values v = getValues(ctx); // strip off the prefix, if there is one. String fullKey = key.toString(); String partialKey = fullKey.substring(fullKey.indexOf(".") + 1); Object obj = null; int xstate = getExtendedState(ctx, v); // check the cache tmpKey.init(partialKey, xstate); obj = v.cache.get(tmpKey); boolean wasInCache = obj != null; if (!wasInCache){ // Search exact matching states and then lesser matching states RuntimeState s = null; int[] lastIndex = new int[] {-1}; while (obj == null && (s = getNextState(v.states, lastIndex, xstate)) != null) { obj = s.defaults.get(partialKey); } // Search Region Defaults if (obj == null && v.defaults != null) { obj = v.defaults.get(partialKey); } // return found object // Search UIManager Defaults if (obj == null) obj = UIManager.get(fullKey); // Search Synth Defaults for InputMaps if (obj == null && partialKey.equals("focusInputMap")) { obj = super.get(ctx, fullKey); } // if all we got was a null, store this fact for later use v.cache.put(new CacheKey(partialKey, xstate), obj == null ? NULL : obj); } // return found object return obj == NULL ? null : obj; } /** * Gets the appropriate background Painter, if there is one, for the state * specified in the given SynthContext. This method does appropriate * fallback searching, as described in #get. * * @param ctx The SynthContext. Must not be null. * @return The background painter associated for the given state, or null if * none could be found. */ public Painter getBackgroundPainter(SynthContext ctx) { Values v = getValues(ctx); int xstate = getExtendedState(ctx, v); Painter p = null; // check the cache tmpKey.init("backgroundPainter$$instance", xstate); p = (Painter)v.cache.get(tmpKey); if (p != null) return p; // not in cache, so lookup and store in cache RuntimeState s = null; int[] lastIndex = new int[] {-1}; while ((s = getNextState(v.states, lastIndex, xstate)) != null) { if (s.backgroundPainter != null) { p = s.backgroundPainter; break; } } if (p == null) p = (Painter)get(ctx, "backgroundPainter"); if (p != null) { v.cache.put(new CacheKey("backgroundPainter$$instance", xstate), p); } return p; } /** * Gets the appropriate foreground Painter, if there is one, for the state * specified in the given SynthContext. This method does appropriate * fallback searching, as described in #get. * * @param ctx The SynthContext. Must not be null. * @return The foreground painter associated for the given state, or null if * none could be found. */ public Painter getForegroundPainter(SynthContext ctx) { Values v = getValues(ctx); int xstate = getExtendedState(ctx, v); Painter p = null; // check the cache tmpKey.init("foregroundPainter$$instance", xstate); p = (Painter)v.cache.get(tmpKey); if (p != null) return p; // not in cache, so lookup and store in cache RuntimeState s = null; int[] lastIndex = new int[] {-1}; while ((s = getNextState(v.states, lastIndex, xstate)) != null) { if (s.foregroundPainter != null) { p = s.foregroundPainter; break; } } if (p == null) p = (Painter)get(ctx, "foregroundPainter"); if (p != null) { v.cache.put(new CacheKey("foregroundPainter$$instance", xstate), p); } return p; } /** * Gets the appropriate border Painter, if there is one, for the state * specified in the given SynthContext. This method does appropriate * fallback searching, as described in #get. * * @param ctx The SynthContext. Must not be null. * @return The border painter associated for the given state, or null if * none could be found. */ public Painter getBorderPainter(SynthContext ctx) { Values v = getValues(ctx); int xstate = getExtendedState(ctx, v); Painter p = null; // check the cache tmpKey.init("borderPainter$$instance", xstate); p = (Painter)v.cache.get(tmpKey); if (p != null) return p; // not in cache, so lookup and store in cache RuntimeState s = null; int[] lastIndex = new int[] {-1}; while ((s = getNextState(v.states, lastIndex, xstate)) != null) { if (s.borderPainter != null) { p = s.borderPainter; break; } } if (p == null) p = (Painter)get(ctx, "borderPainter"); if (p != null) { v.cache.put(new CacheKey("borderPainter$$instance", xstate), p); } return p; } /** * Utility method which returns the proper Values based on the given * SynthContext. Ensures that parsing of the values has occurred, or * reoccurs as necessary. * * @param ctx The SynthContext * @return a non-null values reference */ private Values getValues(SynthContext ctx) { validate(); return values; } /** * Simple utility method that searchs the given array of Strings for the * given string. This method is only called from getExtendedState if * the developer has specified a specific state for the component to be * in (ie, has "wedged" the component in that state) by specifying * they client property "Nimbus.State". * * @param names a non-null array of strings * @param name the name to look for in the array * @return true or false based on whether the given name is in the array */ private boolean contains(String[] names, String name) { assert name != null; for (int i=0; iIn addition, this method checks the component in the given context * for a client property called "Nimbus.State". If one exists, then it will * decompose the String associated with that property to determine what * state to return. In this way, the developer can force a component to be * in a specific state, regardless of what the "real" state of the component * is.
* *The string associated with "Nimbus.State" would be of the form: *
Enabled+CustomState+MouseOver* * @param ctx * @param v * @return */ private int getExtendedState(SynthContext ctx, Values v) { JComponent c = ctx.getComponent(); int xstate = 0; int mask = 1; //check for the Nimbus.State client property //Performance NOTE: getClientProperty ends up inside a synchronized //block, so there is some potential for performance issues here, however //I'm not certain that there is one on a modern VM. Object property = c.getClientProperty("Nimbus.State"); if (property != null) { String stateNames = property.toString(); String[] states = stateNames.split("\\+"); if (v.stateTypes == null){ // standard states only for (String stateStr : states) { State.StandardState s = State.getStandardState(stateStr); if (s != null) xstate |= s.getState(); } } else { // custom states for (State s : v.stateTypes) { if (contains(states, s.getName())) { xstate |= mask; } mask <<= 1; } } } else { //if there are no custom states defined, then simply return the //state that Synth reported if (v.stateTypes == null) return ctx.getComponentState(); //there are custom states on this values, so I'll have to iterate //over them all and return a custom extended state int state = ctx.getComponentState(); for (State s : v.stateTypes) { if (s.isInState(c, state)) { xstate |= mask; } mask <<= 1; } } return xstate; } /** *
Gets the RuntimeState that most closely matches the state in the given * context, but is less specific than the given "lastState". Essentially, * this allows you to search for the next best state.
* *For example, if you had the following three states: *
* Enabled * Enabled+Pressed * Disabled ** And you wanted to find the state that best represented * ENABLED+PRESSED+FOCUSED and
lastState
was null (or an
* empty array, or an array with a single int with index == -1), then
* Enabled+Pressed would be returned. If you then call this method again but
* pass the index of Enabled+Pressed as the "lastState", then
* Enabled would be returned. If you call this method a third time and pass
* the index of Enabled in as the lastState
, then null would be
* returned.
*
* The actual code path for determining the proper state is the same as * in Synth.
* * @param ctx * @param lastState a 1 element array, allowing me to do pass-by-reference. * @return */ private RuntimeState getNextState(RuntimeState[] states, int[] lastState, int xstate) { // Use the StateInfo with the most bits that matches that of state. // If there are none, then fallback to // the StateInfo with a state of 0, indicating it'll match anything. // Consider if we have 3 StateInfos a, b and c with states: // SELECTED, SELECTED | ENABLED, 0 // // Input Return Value // ----- ------------ // SELECTED a // SELECTED | ENABLED b // MOUSE_OVER c // SELECTED | ENABLED | FOCUSED b // ENABLED c if (states != null && states.length > 0) { int bestCount = 0; int bestIndex = -1; int wildIndex = -1; //if xstate is 0, then search for the runtime state with component //state of 0. That is, find the exact match and return it. if (xstate == 0) { for (int counter = states.length - 1; counter >= 0; counter--) { if (states[counter].state == 0) { lastState[0] = counter; return states[counter]; } } //an exact match couldn't be found, so there was no match. lastState[0] = -1; return null; } //xstate is some value != 0 //determine from which index to start looking. If lastState[0] is -1 //then we know to start from the end of the state array. Otherwise, //we start at the lastIndex - 1. int lastStateIndex = lastState == null || lastState[0] == -1 ? states.length : lastState[0]; for (int counter = lastStateIndex - 1; counter >= 0; counter--) { int oState = states[counter].state; if (oState == 0) { if (wildIndex == -1) { wildIndex = counter; } } else if ((xstate & oState) == oState) { // This is key, we need to make sure all bits of the // StateInfo match, otherwise a StateInfo with // SELECTED | ENABLED would match ENABLED, which we // don't want. // This comes from BigInteger.bitCnt int bitCount = oState; bitCount -= (0xaaaaaaaa & bitCount) >>> 1; bitCount = (bitCount & 0x33333333) + ((bitCount >>> 2) & 0x33333333); bitCount = bitCount + (bitCount >>> 4) & 0x0f0f0f0f; bitCount += bitCount >>> 8; bitCount += bitCount >>> 16; bitCount = bitCount & 0xff; if (bitCount > bestCount) { bestIndex = counter; bestCount = bitCount; } } } if (bestIndex != -1) { lastState[0] = bestIndex; return states[bestIndex]; } if (wildIndex != -1) { lastState[0] = wildIndex; return states[wildIndex]; } } lastState[0] = -1; return null; } /** * Contains values such as the UIDefaults and painters asssociated with * a state. WhereasState
represents a distinct state that a
* component can be in (such as Enabled), this class represents the colors,
* fonts, painters, etc associated with some state for this
* style.
*/
private final class RuntimeState implements Cloneable {
int state;
Painter backgroundPainter;
Painter foregroundPainter;
Painter borderPainter;
String stateName;
UIDefaults defaults = new UIDefaults(10, .7f);
private RuntimeState(int state, String stateName) {
this.state = state;
this.stateName = stateName;
}
@Override
public String toString() {
return stateName;
}
@Override
public RuntimeState clone() {
RuntimeState clone = new RuntimeState(state, stateName);
clone.backgroundPainter = backgroundPainter;
clone.foregroundPainter = foregroundPainter;
clone.borderPainter = borderPainter;
clone.defaults.putAll(defaults);
return clone;
}
}
/**
* Essentially a struct of data for a style. A default instance of this
* class is used by NimbusStyle. Additional instances exist for each
* component that has overrides.
*/
private static final class Values {
/**
* The list of State types. A State represents a type of state, such
* as Enabled, Default, WindowFocused, etc. These can be custom states.
*/
State[] stateTypes = null;
/**
* The list of actual runtime state representations. These can represent things such
* as Enabled + Focused. Thus, they differ from States in that they contain
* several states together, and have associated properties, data, etc.
*/
RuntimeState[] states = null;
/**
* The content margins for this region.
*/
Insets contentMargins;
/**
* Defaults on the region/component level.
*/
UIDefaults defaults = new UIDefaults(10, .7f);
/**
* Simple cache. After a value has been looked up, it is stored
* in this cache for later retrieval. The key is a concatenation of
* the property being looked up, two dollar signs, and the extended
* state. So for example:
*
* foo.bar$$2353
*/
Map