/* * Copyright (c) 2005, 2014, 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:

* *

 *     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.

* * @author Richard Bair * @author Jasper Potts */ public final class NimbusStyle extends SynthStyle { /* Keys and scales for large/small/mini components, based on Apples sizes */ /** Large key */ public static final String LARGE_KEY = "large"; /** Small key */ public static final String SMALL_KEY = "small"; /** Mini key */ public static final String MINI_KEY = "mini"; /** Large scale */ public static final double LARGE_SCALE = 1.15; /** Small scale */ public static final double SMALL_SCALE = 0.857; /** Mini scale */ 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 getPainter(Map defaults, String key) { Object p = defaults.get(key); if (p instanceof UIDefaults.LazyValue) { p = ((UIDefaults.LazyValue)p).createValue(UIManager.getDefaults()); } @SuppressWarnings("unchecked") Painter tmp = (p instanceof Painter ? (Painter)p : null); return tmp; } /** * {@inheritDoc} * * Overridden to cause this style to populate itself with data from * UIDefaults, if necessary. */ @Override public Insets getInsets(SynthContext ctx, Insets in) { if (in == null) { in = new Insets(0, 0, 0, 0); } Values v = getValues(ctx); if (v.contentMargins == null) { in.bottom = in.top = in.left = in.right = 0; return in; } else { in.bottom = v.contentMargins.bottom; in.top = v.contentMargins.top; in.left = v.contentMargins.left; in.right = v.contentMargins.right; // Account for scale // The key "JComponent.sizeVariant" is used to match Apple's LAF String scaleKey = (String)ctx.getComponent().getClientProperty( "JComponent.sizeVariant"); if (scaleKey != null){ if (LARGE_KEY.equals(scaleKey)){ in.bottom *= LARGE_SCALE; in.top *= LARGE_SCALE; in.left *= LARGE_SCALE; in.right *= LARGE_SCALE; } else if (SMALL_KEY.equals(scaleKey)){ in.bottom *= SMALL_SCALE; in.top *= SMALL_SCALE; in.left *= SMALL_SCALE; in.right *= SMALL_SCALE; } else if (MINI_KEY.equals(scaleKey)){ in.bottom *= MINI_SCALE; in.top *= MINI_SCALE; in.left *= MINI_SCALE; in.right *= MINI_SCALE; } } return in; } } /** * {@inheritDoc} * *

Overridden to cause this style to populate itself with data from * UIDefaults, if necessary.

* *

In addition, NimbusStyle handles ColorTypes slightly differently from * Synth.

*
    *
  • ColorType.BACKGROUND will equate to the color stored in UIDefaults * named "background".
  • *
  • ColorType.TEXT_BACKGROUND will equate to the color stored in * UIDefaults named "textBackground".
  • *
  • ColorType.FOREGROUND will equate to the color stored in UIDefaults * named "textForeground".
  • *
  • ColorType.TEXT_FOREGROUND will equate to the color stored in * UIDefaults named "textForeground".
  • *
*/ @Override protected Color getColorForState(SynthContext ctx, ColorType type) { String key = null; if (type == ColorType.BACKGROUND) { key = "background"; } else if (type == ColorType.FOREGROUND) { //map FOREGROUND as TEXT_FOREGROUND key = "textForeground"; } else if (type == ColorType.TEXT_BACKGROUND) { key = "textBackground"; } else if (type == ColorType.TEXT_FOREGROUND) { key = "textForeground"; } else if (type == ColorType.FOCUS) { key = "focus"; } else if (type != null) { key = type.toString(); } else { return DEFAULT_COLOR; } Color c = (Color) get(ctx, key); //if all else fails, return a default color (which is a ColorUIResource) if (c == null) c = DEFAULT_COLOR; return c; } /** * {@inheritDoc} * * Overridden to cause this style to populate itself with data from * UIDefaults, if necessary. If a value named "font" is not found in * UIDefaults, then the "defaultFont" font in UIDefaults will be returned * instead. */ @Override protected Font getFontForState(SynthContext ctx) { Font f = (Font)get(ctx, "font"); if (f == null) f = UIManager.getFont("defaultFont"); // Account for scale // The key "JComponent.sizeVariant" is used to match Apple's LAF String scaleKey = (String)ctx.getComponent().getClientProperty( "JComponent.sizeVariant"); if (scaleKey != null){ if (LARGE_KEY.equals(scaleKey)){ f = f.deriveFont(Math.round(f.getSize2D()*LARGE_SCALE)); } else if (SMALL_KEY.equals(scaleKey)){ f = f.deriveFont(Math.round(f.getSize2D()*SMALL_SCALE)); } else if (MINI_KEY.equals(scaleKey)){ f = f.deriveFont(Math.round(f.getSize2D()*MINI_SCALE)); } } return f; } /** * {@inheritDoc} * * Returns the SynthPainter for this style, which ends up delegating to * the Painters installed in this style. */ @Override public SynthPainter getPainter(SynthContext ctx) { return painter; } /** * {@inheritDoc} * * Overridden to cause this style to populate itself with data from * UIDefaults, if necessary. If opacity is not specified in UI defaults, * then it defaults to being non-opaque. */ @Override public boolean isOpaque(SynthContext ctx) { // Force Table CellRenderers to be opaque if ("Table.cellRenderer".equals(ctx.getComponent().getName())) { return true; } Boolean opaque = (Boolean)get(ctx, "opaque"); return opaque == null ? false : opaque; } /** * {@inheritDoc} * *

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 SynthContext identifying requester * @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; } @SuppressWarnings("unchecked") private static Painter paintFilter(@SuppressWarnings("rawtypes") Painter painter) { return (Painter) painter; } /** * 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 = paintFilter((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 = paintFilter(s.backgroundPainter); break; } } if (p == null) p = paintFilter((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 = paintFilter((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 = paintFilter(s.foregroundPainter); break; } } if (p == null) p = paintFilter((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 = paintFilter((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 = paintFilter(s.borderPainter); break; } } if (p == null) p = paintFilter((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 searches 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; iGets the extended state for a given synth context. Nimbus supports the * ability to define custom states. The algorithm used for choosing what * style information to use for a given state requires a single integer * bit string where each bit in the integer represents a different state * that the component is in. This method uses the componentState as * reported in the SynthContext, in addition to custom states, to determine * what this extended state is.

* *

In 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 */ @SuppressWarnings({"unchecked", "rawtypes"}) 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 associated with * a state. Whereas State 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 cache = new HashMap(); } /** * This implementation presupposes that key is never null and that * the two keys being checked for equality are never null */ private static final class CacheKey { private String key; private int xstate; CacheKey(Object key, int xstate) { init(key, xstate); } void init(Object key, int xstate) { this.key = key.toString(); this.xstate = xstate; } @Override public boolean equals(Object obj) { final CacheKey other = (CacheKey) obj; if (obj == null) return false; if (this.xstate != other.xstate) return false; if (!this.key.equals(other.key)) return false; return true; } @Override public int hashCode() { int hash = 3; hash = 29 * hash + this.key.hashCode(); hash = 29 * hash + this.xstate; return hash; } } }