Package com.seaglasslookandfeel

Source Code of com.seaglasslookandfeel.SeaGlassStyle

/*
* Copyright (c) 2009 Kathryn Huxtable and Kenneth Orr.
*
* This file is part of the SeaGlass Pluggable Look and Feel.
*
* 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.
*
* $Id: SeaGlassStyle.java 1595 2011-08-09 20:33:48Z rosstauscher@gmx.de $
*/
package com.seaglasslookandfeel;

import static javax.swing.plaf.synth.SynthConstants.DEFAULT;
import static javax.swing.plaf.synth.SynthConstants.DISABLED;
import static javax.swing.plaf.synth.SynthConstants.ENABLED;
import static javax.swing.plaf.synth.SynthConstants.FOCUSED;
import static javax.swing.plaf.synth.SynthConstants.MOUSE_OVER;
import static javax.swing.plaf.synth.SynthConstants.PRESSED;
import static javax.swing.plaf.synth.SynthConstants.SELECTED;

import java.awt.Color;
import java.awt.Font;
import java.awt.Insets;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
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;

import javax.swing.JComponent;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.synth.ColorType;
import javax.swing.plaf.synth.SynthContext;
import javax.swing.plaf.synth.SynthGraphicsUtils;
import javax.swing.plaf.synth.SynthPainter;
import javax.swing.plaf.synth.SynthStyle;

import sun.awt.AppContext;

import com.seaglasslookandfeel.component.SeaGlassBorder;
import com.seaglasslookandfeel.painter.SeaGlassPainter;
import com.seaglasslookandfeel.state.State;
import com.seaglasslookandfeel.ui.SeaglassUI;
import com.seaglasslookandfeel.util.SeaGlassGraphicsUtils;

/**
* <p>A SynthStyle implementation used by SeaGlass. Each Region that has been
* registered with the SeaGlassLookAndFeel will have an associated
* SeaGlassStyle. Third party components that are registered with the
* SeaGlassLookAndFeel will therefore be handed a SeaGlassStyle from the look
* and feel from the #getStyle(JComponent, Region) method.</p>
*
* <p>Based on NimbusStyle by Richard Bair and Jasper Potts. Reimplemented
* because too much is package local.</p>
*
* <p>This class properly reads and retrieves values placed in the UIDefaults
* according to the standard SeaGlass/Nimbus naming conventions. It will create
* and retrieve painters, fonts, colors, and other data stored there.</p>
*
* <p>SeaGlassStyle also supports the ability to override settings on a per
* component basis. SeaGlassStyle checks the component's client property map for
* "SeaGlass.Overrides". If the value associated with this key is an instance of
* UIDefaults, then the values in that defaults table will override the standard
* SeaGlass defaults in UIManager, but for that component instance only.</p>
*
* <p>Optionally, you may specify the client property
* "SeaGlass.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.</p>
*
* <p>You must specify "SeaGlass.Overrides" for
* "SeaGlass.Overrides.InheritDefaults" to have any effect. "SeaGlass.Overrides"
* indicates whether there are any overrides, while
* "SeaGlass.Overrides.InheritDefaults" indicates whether those overrides should
* first be initialized with the defaults from UIManager.</p>
*
* <p>The SeaGlassStyle is reloaded whenever a property change event is fired
* for a component for "SeaGlass.Overrides" or
* "SeaGlass.Overrides.InheritDefaults". So for example, setting a new
* UIDefaults on a component would cause the style to be reloaded.</p>
*
* <p>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 SeaGlassStyle and read a new one from SeaGlassLookAndFeel
* using SeaGlassLookAndFeel.getStyle.</p>
*
* <p>The primary API of interest in this class for 3rd party component authors
* are the three methods which retrieve painters: #getBackgroundPainter,
* #getForegroundPainter, and #getBorderPainter.</p>
*
* <p>SeaGlassStyle allows you to specify custom states, or modify the order of
* states. Synth (and thus Nimbus and SeaGlass) 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.</p>
*
* <p>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 SeaGlass, you
* specify these custom states by including a special key in UIDefaults. The
* following UIDefaults entries define three states for this button:</p>
*
* <pre>
* <code>
*     JButton.States = Enabled, Disabled, Toolbar
*     JButton[Enabled].backgroundPainter = somePainter
*     JButton[Disabled].background = BLUE
*     JButton[Toolbar].backgroundPainter = someOtherPaint
* </code>
* </pre>
*
* <p>As you can see, the <code>JButton.States</code> entry lists the states
* that the JButton style will support. You then specify the settings for each
* state. If you do not specify the <code>JButton.States</code> 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.
* </p>
*/
public class SeaGlassStyle extends SynthStyle {

    // Keys and scales for large/small/mini components, based on Apples sizes.
    /** "Large" size variant. */
    public static final String LARGE_KEY = "large";

    /** "Small" size variant. */
    public static final String SMALL_KEY = "small";

    /** "Mini" size variant. */
    public static final String MINI_KEY = "mini";
   
    /** "Natural" size variant. */
    public static final String NATURAL_KEY = "natural";

    /** Scale factor for "large" size variant. */
    public static final double LARGE_SCALE = 1.15;

    /** Scale factor for "small" size variant. */
    public static final double SMALL_SCALE = 0.857;

    /** Scale factor for "mini" size variant. */
    public static final double MINI_SCALE = 0.714;

    /** Scale factor for "natural" size variant. */
    public static final double NATURAL_SCALE = 1.0;

    /**
     * Gets the size variant that is applicable for the given component.
     * @param c
     * @return
     */
    public static String getSizeVariant(JComponent c) {
        String sizeVariant = System.getProperty("JComponent.sizeVariant");
        String componentSizeVariant = (String)c.getClientProperty("JComponent.sizeVariant");
        if (componentSizeVariant != null) {
            sizeVariant  = componentSizeVariant;
        }
        return sizeVariant;
    }
   
   
    /**
     * 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';

    /**
     * Simple Comparator for ordering the RuntimeStates according to their rank.
     */
    private static final Comparator<RuntimeState> STATE_COMPARATOR = new Comparator<RuntimeState>() {
        public int compare(RuntimeState a, RuntimeState b) {
            return a.state - b.state;
        }
    };

    /** Shared SynthGraphics. */
    private static final SynthGraphicsUtils SEAGLASS_GRAPHICS = new SeaGlassGraphicsUtils();

    /**
     * <p>The Color to return from getColorForState if it would otherwise have
     * returned null.</p>
     *
     * <p>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 <code>null</code> is returned from
     * getColorForState, then thereafter the color is not updated for other
     * states or on LAF changes or updates. This defaultColor is used to ensure
     * that a ColorUIResource is always returned from getColorForState.</p>
     */
    private Color defaultColor = UIManager.getColor("seaGlassStyleDefaultColor");

    /**
     * The prefix for the component or region that this SeaGlassStyle
     * 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 SeaGlassStyle. The
     * SynthPainter returned will be a SynthPainterImpl, which will in turn
     * delegate back to this SeaGlassStyle 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 SeaGlassStyles are created for a specific component only. In
     * SeaGlass, this happens whenever the component has as a client property a
     * UIDefaults which overrides (or supplements) those defaults found in
     * UIManager.
     */
    private JComponent component;

    /**
     * Create a new SeaGlassStyle. 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
     *               SeaGlassStyle should be associated with. This is only used
     *               when the component has SeaGlass overrides registered in its
     *               client properties and should be null otherwise.
     */
    SeaGlassStyle(String prefix, JComponent c) {
        this.component = c;
        this.prefix    = prefix;
        this.painter   = new SeaGlassSynthPainterImpl(this);
    }

    /**
     * Returns the <code>SynthGraphicUtils</code> for the specified context.
     *
     * @param  context SynthContext identifying requester
     *
     * @return SynthGraphicsUtils
     */
    public SynthGraphicsUtils getGraphicsUtils(SynthContext context) {
        return SEAGLASS_GRAPHICS;
    }

    /**
     * Install UI defaults.
     *
     * @param context the SynthContext describing the component/regiont, the
     *                style, and the state.
     * @param ui      the UI delegate.
     */
    public void installDefaults(SeaGlassContext context, SeaglassUI ui) {
        // Special case the Border as this will likely change when the LAF
        // can have more control over this.
        if (!context.isSubregion()) {
            JComponent c      = context.getComponent();
            Border     border = c.getBorder();

            if (border == null || border instanceof UIResource) {
                c.setBorder(new SeaGlassBorder(ui, getInsets(context, null)));
            }
        }

        installDefaults(context);
    }

    /**
     * Installs the necessary state from this Style on the <code>
     * JComponent</code> from <code>context</code>.
     *
     * <p>Overridden to cause this style to populate itself with data from
     * UIDefaults, if necessary.</p>
     *
     * @param ctx context SynthContext identifying component to install
     *            properties to.
     */
    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);
    }

    /**
     * Internal method to parse a style prefix.
     *
     * @param  key the key to parse.
     *
     * @return the prefix portion of the key.
     */
    static String parsePrefix(String key) {
        if (key == null)
            return null;

        boolean inquotes = false;

        for (int i = 0; i < key.length(); i++) {
            char c = key.charAt(i);

            if (c == '"') {
                inquotes = !inquotes;
            } else if ((c == '[' || c == '.') && !inquotes) {
                return key.substring(0, i);
            }
        }

        return null;
    }

    /**
     * Called by SeaGlassLookAndFeel when the look and feel is being
     * uninstalled. Performs general cleanup of any app-context specific data.
     */
    static void uninitialize() {
        // get the appcontext that we've stored data in
        AppContext ctx = AppContext.getAppContext();

        // get the pcl stored in app context
        PropertyChangeListener pcl = (PropertyChangeListener) ctx.get("SeaGlassStyle.defaults.pcl");

        // if the pcl exists, uninstall it from the UIDefaults tables
        if (pcl != null) {
            UIManager.getDefaults().removePropertyChangeListener(pcl);
            UIManager.getLookAndFeelDefaults().removePropertyChangeListener(pcl);
        }

        // clear out the compiled defaults
        ctx.put("SeaGlassStyle.defaults", null);
    }

    /**
     * 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 SeaGlassStyle 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 SeaGlass.Overrides)
        values = new Values();

        // the profiler revealed that a great deal of CPU time and useless
        // garbage was being produced by this method and the init method. One
        // culprit was the creation and reparsing of the entire UIDefaults
        // map on each call to this method where "values" was null. It turns
        // out this was happening a lot.
        // To remove this bottleneck, we store the compiled TreeMaps of defaults
        // in the appContext for reuse. It is nulled whenever the UIDefaults
        // changes and recomputed when necessary.
        final AppContext ctx = AppContext.getAppContext();

        // fetch the defaults from the app context. If null, then create and
        // store the compiled defaults
        Map<String, TreeMap<String, Object>> compiledDefaults = (Map<String, TreeMap<String, Object>>) ctx.get("SeaGlassStyle.defaults");

        if (compiledDefaults == null) {

            // the entire UIDefaults tables are parsed and compiled into
            // this map of maps. The key of the compiledDefaults is the
            // prefix for each style, while the value is a map of
            // keys->values for that prefix.
            compiledDefaults = new HashMap<String, TreeMap<String, Object>>();

            // get all the defaults from UIManager.getDefaults() and put them
            // into the compiledDefaults
            compileDefaults(compiledDefaults, UIManager.getDefaults());

            // This second statement pulls defaults from the laf defaults
            UIDefaults lafDefaults = UIManager.getLookAndFeelDefaults();

            compileDefaults(compiledDefaults, lafDefaults);

            // if it has not already been done, add a listener to both
            // UIManager.getDefaults() and UIManager.getLookAndFeelDefaults().
            PropertyChangeListener pcl = (PropertyChangeListener) ctx.get("SeaGlassStyle.defaults.pcl");

            // if pcl is null, then it has not yet been registered with
            // the UIManager defaults for this app context
            if (pcl == null) {

                // create a PCL which will simply clear out the compiled
                // defaults from the app context, causing it to be recomputed
                // on subsequent passes
                pcl = new DefaultsListener();

                // add the PCL to both defaults tables that we pay attention
                // to, so that if the UIDefaults are updated, then the
                // precompiled defaults will be cleared from the app context
                // and recomputed on subsequent passes
                UIManager.getDefaults().addPropertyChangeListener(pcl);
                UIManager.getLookAndFeelDefaults().addPropertyChangeListener(pcl);

                // save the PCL to the app context as a marker indicating
                // that the PCL has been registered so we don't end up adding
                // more than one listener to the UIDefaults tables.
                ctx.put("SeaGlassStyle.defaults.pcl", pcl);
            }

            // store the defaults for reuse
            ctx.put("SeaGlassStyle.defaults", compiledDefaults);
        }

        TreeMap<String, Object> defaults = compiledDefaults.get(prefix);
        if (defaults == null) {
            defaults = new TreeMap<String, Object>();
        }

        // inspect the client properties for the key "SeaGlass.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) {
            Object o = component.getClientProperty("SeaGlass.Overrides");

            if (o instanceof UIDefaults) {
                Object                  i       = component.getClientProperty("SeaGlass.Overrides.InheritDefaults");
                boolean                 inherit = i instanceof Boolean ? (Boolean) i : true;
                UIDefaults              d       = (UIDefaults) o;
                TreeMap<String, Object> map     = new TreeMap<String, Object>();

                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;
                }
            }
        }

        // Now that I've accumulated all the defaults pertaining to this
        // style, call init which will read these defaults and configure
        // the default "values".
        init(values, defaults);
    }

    /**
     * Iterates over all the keys in the specified UIDefaults and compiles those
     * keys into the comiledDefaults data structure. It relies on parsing the
     * "prefix" out of the key. If the key is not a String or is null then it is
     * ignored. In all other cases a prefix is parsed out (even if that prefix
     * is the empty String or is a "fake" prefix. That is, suppose you had a key
     * Foo~~MySpecial.KeyThing~~. In this case this is not a SeaGlass formatted
     * key, but we don't care, we treat it as if it is. This doesn't pose any
     * harm, it will simply never be used).
     *
     * @param compiledDefaults the compiled defaults data structure.
     * @param d                the UIDefaults to be parsed.
     */
    private void compileDefaults(Map<String, TreeMap<String, Object>> compiledDefaults, UIDefaults d) {
        for (Map.Entry<Object, Object> entry : d.entrySet()) {

            if (entry.getKey() instanceof String) {
                String key = (String) entry.getKey();
                String kp  = parsePrefix(key);

                if (kp == null)
                    continue;

                TreeMap<String, Object> map = compiledDefaults.get(kp);

                if (map == null) {
                    map = new TreeMap<String, Object>();
                    compiledDefaults.put(kp, map);
                }

                map.put(key, entry.getValue());
            }
        }
    }

    /**
     * Initializes the given <code>Values</code> object with the defaults
     * contained in the given TreeMap.
     *
     * @param v          The Values object to be initialized
     * @param myDefaults a map of UIDefaults to use in initializing the Values.
     *                   This map must contain only keys associated with this
     *                   Style.
     */
    private void init(Values v, TreeMap<String, Object> myDefaults) {
        // 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<State> states = new ArrayList<State>();

        // a map of state name to code
        Map<String, Integer> stateCodes = new HashMap<String, Integer>();

        // This is a list of runtime "state" context objects. These contain
        // the values associated with each state.
        List<RuntimeState> runtimeStates = new ArrayList<RuntimeState>();

        // 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) myDefaults.get(prefix + ".States");

        if (statesString != null) {
            String[] s = statesString.split(",");

            for (int i = 0; i < s.length; i++) {
                s[i] = s[i].trim();

                if (!State.isStandardStateName(s[i])) {

                    // this is a non-standard state name, so look for the
                    // custom state associated with it
                    String stateName   = prefix + "." + s[i];
                    State  customState = (State) myDefaults.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) {
                v.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 : myDefaults.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/SeaGlass). 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)) {
                    v.contentMargins = (Insets) myDefaults.get(key);
                } else if ("States".equals(property)) {
                    // ignore
                } else {
                    v.defaults.put(property, myDefaults.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(myDefaults, key);
                } else if ("foregroundPainter".equals(property)) {
                    rs.foregroundPainter = getPainter(myDefaults, key);
                } else if ("borderPainter".equals(property)) {
                    rs.borderPainter = getPainter(myDefaults, key);
                } else {
                    rs.defaults.put(property, myDefaults.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
        v.states = runtimeStates.toArray(new RuntimeState[runtimeStates.size()]);
    }

    /**
     * Determine the painter given the defaults and a key.
     *
     * @param  defaults the defaults.
     * @param  key      the key.
     *
     * @return the painter, if any can be found, {@code null} otherwise.
     */
    private SeaGlassPainter getPainter(TreeMap<String, Object> defaults, String key) {
        Object p = defaults.get(key);

        if (p instanceof UIDefaults.LazyValue) {
            p = ((UIDefaults.LazyValue) p).createValue(UIManager.getDefaults());
        }

        return (p instanceof SeaGlassPainter ? (SeaGlassPainter) p : null);
    }

    /**
     * Returns the Insets that are used to calculate sizing information.
     *
     * <p>Overridden to cause this style to populate itself with data from
     * UIDefaults, if necessary.</p>
     *
     * @param  ctx context SynthContext identifying requester
     * @param  in  insets Insets to place return value in.
     *
     * @return Sizing Insets.
     */
    @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 = SeaGlassStyle.getSizeVariant(ctx.getComponent());
            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;
        }
    }

    /**
     * Returns the color for the specified state. This should NOT call any
     * methods on the <code>JComponent</code>.
     *
     * <p>Overridden to cause this style to populate itself with data from
     * UIDefaults, if necessary.</p>
     *
     * <p>In addition, SeaGlassStyle handles ColorTypes slightly differently
     * from Synth.</p>
     *
     * <ul>
     *   <li>ColorType.BACKGROUND will equate to the color stored in UIDefaults
     *     named "background".</li>
     *   <li>ColorType.TEXT_BACKGROUND will equate to the color stored in
     *     UIDefaults named "textBackground".</li>
     *   <li>ColorType.FOREGROUND will equate to the color stored in UIDefaults
     *     named "textForeground".</li>
     *   <li>ColorType.TEXT_FOREGROUND will equate to the color stored in
     *     UIDefaults named "textForeground".</li>
     * </ul>
     *
     * @param  ctx  context SynthContext identifying requester
     * @param  type Type of color being requested.
     *
     * @return Color to render with
     */
    @Override
    public 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 defaultColor;
        }

        Color c = (Color) get(ctx, key);

        // if all else fails, return a default color (which is a
        // ColorUIResource)
        if (c == null)
            c = defaultColor;

        return c;
    }

    /**
     * Returns the font for the specified state. This should NOT call any method
     * on the <code>JComponent</code>.
     *
     * <p>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.</p>
     *
     * @param  ctx context SynthContext identifying requester
     *
     * @return Font to render with
     */
    @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 = SeaGlassStyle.getSizeVariant(ctx.getComponent());
        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;

    }

    /**
     * Returns the <code>SynthPainter</code> that will be used for painting.
     * This ends up delegating to the Painters installed in this style. It may
     * return null;
     *
     * @param  ctx context SynthContext identifying requester
     *
     * @return SynthPainter to use
     */
    @Override
    public SynthPainter getPainter(SynthContext ctx) {
        return painter;
    }

    /**
     * Returns true if the region is opaque.
     *
     * <p>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.</p>
     *
     * @param  ctx context SynthContext identifying requester
     *
     * @return true if region is 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;
    }

    /**
     * Getter for a region specific style property.
     *
     * <p>Overridden to cause this style to populate itself with data from
     * UIDefaults, if necessary.</p>
     *
     * <p>Properties in UIDefaults may be specified in a chained manner. For
     * example:</p>
     *
     * <pre>
     * background
     * Button.opacity
     * Button.Enabled.foreground
     * Button.Enabled+Selected.background
     * </pre>
     *
     * <p>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.</p>
     *
     * <p>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.</p>
     *
     * <p>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 SeaGlassStyle constructor. In either case, it looks for
     * "background".</p>
     *
     * @param  ctx SynthContext identifying requester
     * @param  key Property being requested. Must not be null.
     *
     * @return Value of the named property *
     */
    @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 SeaGlassPainter getBackgroundPainter(SynthContext ctx) {
        Values  v      = getValues(ctx);
        int     xstate = getExtendedState(ctx, v);
        SeaGlassPainter p      = null;

        // check the cache
        tmpKey.init("backgroundPainter$$instance", xstate);
        p = (SeaGlassPainter) 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 = (SeaGlassPainter) 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 SeaGlassPainter getForegroundPainter(SynthContext ctx) {
        Values  v      = getValues(ctx);
        int     xstate = getExtendedState(ctx, v);
        SeaGlassPainter p      = null;

        // check the cache
        tmpKey.init("foregroundPainter$$instance", xstate);
        p = (SeaGlassPainter) 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 = (SeaGlassPainter) 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 SeaGlassPainter getBorderPainter(SynthContext ctx) {
        Values  v      = getValues(ctx);
        int     xstate = getExtendedState(ctx, v);
        SeaGlassPainter p      = null;

        // check the cache
        tmpKey.init("borderPainter$$instance", xstate);
        p = (SeaGlassPainter) 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 = (SeaGlassPainter) 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 "SeaGlass.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; i < names.length; i++) {

            if (name.equals(names[i])) {
                return true;
            }
        }

        return false;
    }

    /**
     * <p>Gets the extended state for a given synth context. SeaGlass 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.</p>
     *
     * <p>In addition, this method checks the component in the given context for
     * a client property called "SeaGlass.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.</p>
     *
     * <p>The string associated with "SeaGlass.State" would be of the form:</p>
     *
     * <pre>
     * Enabled + CustomState + MouseOver
     * </pre>
     *
     * @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 SeaGlass.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("SeaGlass.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;
    }

    /**
     * <p>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.</p>
     *
     * <p>For example, if you had the following three states:</p>
     *
     * <pre>
     * Enabled
     * Enabled+Pressed
     * Disabled
     * </pre>
     *
     * <p>And you wanted to find the state that best represented
     * ENABLED+PRESSED+FOCUSED and <code>lastState</code> 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 <code>lastState</code>, then null would be returned.
     * </p>
     *
     * <p>The actual code path for determining the proper state is the same as
     * in Synth.</p>
     *
     * @param  states
     * @param  lastState a 1 element array, allowing me to do pass-by-reference.
     * @param  xstate
     *
     * @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. Whereas <code>State</code> 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;
        SeaGlassPainter    backgroundPainter;
        SeaGlassPainter    foregroundPainter;
        SeaGlassPainter    borderPainter;
        String     stateName;
        UIDefaults defaults = new UIDefaults(10, .7f);

        /**
         * Creates a new RuntimeState object.
         *
         * @param state     the state.
         * @param stateName the name for the state.
         */
        private RuntimeState(int state, String stateName) {
            this.state     = state;
            this.stateName = stateName;
        }

        /**
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return stateName;
        }

        /**
         * @see java.lang.Object#clone()
         */
        @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 SeaGlassStyle. 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:
         *
         * <p>foo.bar$$2353</p>
         */
        Map<CacheKey, Object> cache = new HashMap<CacheKey, Object>();
    }

    /**
     * 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;

        /**
         * Creates a new CacheKey object.
         *
         * @param key    the key.
         * @param xstate the extended state.
         */
        CacheKey(Object key, int xstate) {
            init(key, xstate);
        }

        /**
         * Initialize the CacheKey object with its values.
         *
         * @param key    the key.
         * @param xstate the extended state.
         */
        void init(Object key, int xstate) {
            this.key    = key.toString();
            this.xstate = xstate;
        }

        /**
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @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;
        }

        /**
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            int hash = 3;

            hash = 29 * hash + this.key.hashCode();
            hash = 29 * hash + this.xstate;

            return hash;
        }
    }

    /**
     * This listener is used to listen to the UIDefaults tables and clear out
     * the cached-precompiled map of defaults in that case.
     */
    private static final class DefaultsListener implements PropertyChangeListener {

        /**
         * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
         */
        public void propertyChange(PropertyChangeEvent evt) {
            AppContext.getAppContext().put("SeaGlassStyle.defaults", null);
        }
    }
}
TOP

Related Classes of com.seaglasslookandfeel.SeaGlassStyle

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.