com.badlogic.gdx.scenes.scene2d.ui.Skin.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.scenes.scene2d.ui.Skin.java

Source

/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * 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.
 ******************************************************************************/

package com.badlogic.gdx.scenes.scene2d.ui;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.NinePatch;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasSprite;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.utils.BaseDrawable;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.scenes.scene2d.utils.NinePatchDrawable;
import com.badlogic.gdx.scenes.scene2d.utils.SpriteDrawable;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
import com.badlogic.gdx.scenes.scene2d.utils.TiledDrawable;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.Json.ReadOnlySerializer;
import com.badlogic.gdx.utils.JsonValue;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.SerializationException;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.Method;
import com.badlogic.gdx.utils.reflect.ReflectionException;

/** A skin stores resources for UI widgets to use (texture regions, ninepatches, fonts, colors, etc). Resources are named and can
 * be looked up by name and type. Resources can be described in JSON. Skin provides useful conversions, such as allowing access to
 * regions in the atlas as ninepatches, sprites, drawables, etc. The get* methods return an instance of the object in the skin.
 * The new* methods return a copy of an instance in the skin.
 * <p>
 * See the <a href="https://code.google.com/p/libgdx/wiki/Skin">documentation</a> for more.
 * @author Nathan Sweet */
public class Skin implements Disposable {
    ObjectMap<Class, ObjectMap<String, Object>> resources = new ObjectMap();
    TextureAtlas atlas;

    /** Creates an empty skin. */
    public Skin() {
    }

    /** Creates a skin containing the resources in the specified skin JSON file. If a file in the same directory with a ".atlas"
     * extension exists, it is loaded as a {@link TextureAtlas} and the texture regions added to the skin. The atlas is
     * automatically disposed when the skin is disposed. */
    public Skin(FileHandle skinFile) {
        FileHandle atlasFile = skinFile.sibling(skinFile.nameWithoutExtension() + ".atlas");
        if (atlasFile.exists()) {
            atlas = new TextureAtlas(atlasFile);
            addRegions(atlas);
        }

        load(skinFile);
    }

    /** Creates a skin containing the resources in the specified skin JSON file and the texture regions from the specified atlas.
     * The atlas is automatically disposed when the skin is disposed. */
    public Skin(FileHandle skinFile, TextureAtlas atlas) {
        this.atlas = atlas;
        addRegions(atlas);
        load(skinFile);
    }

    /** Creates a skin containing the texture regions from the specified atlas. The atlas is automatically disposed when the skin is
     * disposed. */
    public Skin(TextureAtlas atlas) {
        this.atlas = atlas;
        addRegions(atlas);
    }

    /** Adds all resources in the specified skin JSON file. */
    public void load(FileHandle skinFile) {
        try {
            getJsonLoader(skinFile).fromJson(Skin.class, skinFile);
        } catch (SerializationException ex) {
            throw new SerializationException("Error reading file: " + skinFile, ex);
        }
    }

    /** Adds all named texture regions from the atlas. The atlas will not be automatically disposed when the skin is disposed. */
    public void addRegions(TextureAtlas atlas) {
        Array<AtlasRegion> regions = atlas.getRegions();
        for (int i = 0, n = regions.size; i < n; i++) {
            AtlasRegion region = regions.get(i);
            add(region.name, region, TextureRegion.class);
        }
    }

    public void add(String name, Object resource) {
        add(name, resource, resource.getClass());
    }

    public void add(String name, Object resource, Class type) {
        if (name == null)
            throw new IllegalArgumentException("name cannot be null.");
        if (resource == null)
            throw new IllegalArgumentException("resource cannot be null.");
        ObjectMap<String, Object> typeResources = resources.get(type);
        if (typeResources == null) {
            typeResources = new ObjectMap();
            resources.put(type, typeResources);
        }
        typeResources.put(name, resource);
    }

    public void remove(String name, Class type) {
        if (name == null)
            throw new IllegalArgumentException("name cannot be null.");
        ObjectMap<String, Object> typeResources = resources.get(type);
        typeResources.remove(name);
    }

    public <T> T get(Class<T> type) {
        return get("default", type);
    }

    public <T> T get(String name, Class<T> type) {
        if (name == null)
            throw new IllegalArgumentException("name cannot be null.");
        if (type == null)
            throw new IllegalArgumentException("type cannot be null.");

        if (type == Drawable.class)
            return (T) getDrawable(name);
        if (type == TextureRegion.class)
            return (T) getRegion(name);
        if (type == NinePatch.class)
            return (T) getPatch(name);
        if (type == Sprite.class)
            return (T) getSprite(name);

        ObjectMap<String, Object> typeResources = resources.get(type);
        if (typeResources == null)
            throw new GdxRuntimeException("No " + type.getName() + " registered with name: " + name);
        Object resource = typeResources.get(name);
        if (resource == null)
            throw new GdxRuntimeException("No " + type.getName() + " registered with name: " + name);
        return (T) resource;
    }

    public <T> T optional(String name, Class<T> type) {
        if (name == null)
            throw new IllegalArgumentException("name cannot be null.");
        if (type == null)
            throw new IllegalArgumentException("type cannot be null.");
        ObjectMap<String, Object> typeResources = resources.get(type);
        if (typeResources == null)
            return null;
        return (T) typeResources.get(name);
    }

    public boolean has(String name, Class type) {
        ObjectMap<String, Object> typeResources = resources.get(type);
        if (typeResources == null)
            return false;
        return typeResources.containsKey(name);
    }

    /** Returns the name to resource mapping for the specified type, or null if no resources of that type exist. */
    public <T> ObjectMap<String, T> getAll(Class<T> type) {
        return (ObjectMap<String, T>) resources.get(type);
    }

    public Color getColor(String name) {
        return get(name, Color.class);
    }

    public BitmapFont getFont(String name) {
        return get(name, BitmapFont.class);
    }

    /** Returns a registered texture region. If no region is found but a texture exists with the name, a region is created from the
     * texture and stored in the skin. */
    public TextureRegion getRegion(String name) {
        TextureRegion region = optional(name, TextureRegion.class);
        if (region != null)
            return region;

        Texture texture = optional(name, Texture.class);
        if (texture == null)
            throw new GdxRuntimeException("No TextureRegion or Texture registered with name: " + name);
        region = new TextureRegion(texture);
        add(name, region, TextureRegion.class);
        return region;
    }

    /** Returns a registered tiled drawable. If no tiled drawable is found but a region exists with the name, a tiled drawable is
     * created from the region and stored in the skin. */
    public TiledDrawable getTiledDrawable(String name) {
        TiledDrawable tiled = optional(name, TiledDrawable.class);
        if (tiled != null)
            return tiled;

        tiled = new TiledDrawable(getRegion(name));
        tiled.setName(name);
        add(name, tiled, TiledDrawable.class);
        return tiled;
    }

    /** Returns a registered ninepatch. If no ninepatch is found but a region exists with the name, a ninepatch is created from the
     * region and stored in the skin. If the region is an {@link AtlasRegion} then the {@link AtlasRegion#splits} are used,
     * otherwise the ninepatch will have the region as the center patch. */
    public NinePatch getPatch(String name) {
        NinePatch patch = optional(name, NinePatch.class);
        if (patch != null)
            return patch;

        try {
            TextureRegion region = getRegion(name);
            if (region instanceof AtlasRegion) {
                int[] splits = ((AtlasRegion) region).splits;
                if (splits != null) {
                    patch = new NinePatch(region, splits[0], splits[1], splits[2], splits[3]);
                    int[] pads = ((AtlasRegion) region).pads;
                    if (pads != null)
                        patch.setPadding(pads[0], pads[1], pads[2], pads[3]);
                }
            }
            if (patch == null)
                patch = new NinePatch(region);
            add(name, patch, NinePatch.class);
            return patch;
        } catch (GdxRuntimeException ex) {
            throw new GdxRuntimeException("No NinePatch, TextureRegion, or Texture registered with name: " + name);
        }
    }

    /** Returns a registered sprite. If no sprite is found but a region exists with the name, a sprite is created from the region
     * and stored in the skin. If the region is an {@link AtlasRegion} then an {@link AtlasSprite} is used if the region has been
     * whitespace stripped or packed rotated 90 degrees. */
    public Sprite getSprite(String name) {
        Sprite sprite = optional(name, Sprite.class);
        if (sprite != null)
            return sprite;

        try {
            TextureRegion textureRegion = getRegion(name);
            if (textureRegion instanceof AtlasRegion) {
                AtlasRegion region = (AtlasRegion) textureRegion;
                if (region.rotate || region.packedWidth != region.originalWidth
                        || region.packedHeight != region.originalHeight)
                    sprite = new AtlasSprite(region);
            }
            if (sprite == null)
                sprite = new Sprite(textureRegion);
            add(name, sprite, Sprite.class);
            return sprite;
        } catch (GdxRuntimeException ex) {
            throw new GdxRuntimeException("No NinePatch, TextureRegion, or Texture registered with name: " + name);
        }
    }

    /** Returns a registered drawable. If no drawable is found but a region, ninepatch, or sprite exists with the name, then the
     * appropriate drawable is created and stored in the skin. */
    public Drawable getDrawable(String name) {
        Drawable drawable = optional(name, Drawable.class);
        if (drawable != null)
            return drawable;

        drawable = optional(name, TiledDrawable.class);
        if (drawable != null)
            return drawable;

        // Use texture or texture region. If it has splits, use ninepatch. If it has rotation or whitespace stripping, use sprite.
        try {
            TextureRegion textureRegion = getRegion(name);
            if (textureRegion instanceof AtlasRegion) {
                AtlasRegion region = (AtlasRegion) textureRegion;
                if (region.splits != null)
                    drawable = new NinePatchDrawable(getPatch(name));
                else if (region.rotate || region.packedWidth != region.originalWidth
                        || region.packedHeight != region.originalHeight)
                    drawable = new SpriteDrawable(getSprite(name));
            }
            if (drawable == null)
                drawable = new TextureRegionDrawable(textureRegion);
        } catch (GdxRuntimeException ignored) {
        }

        // Check for explicit registration of ninepatch, sprite, or tiled drawable.
        if (drawable == null) {
            NinePatch patch = optional(name, NinePatch.class);
            if (patch != null)
                drawable = new NinePatchDrawable(patch);
            else {
                Sprite sprite = optional(name, Sprite.class);
                if (sprite != null)
                    drawable = new SpriteDrawable(sprite);
                else
                    throw new GdxRuntimeException(
                            "No Drawable, NinePatch, TextureRegion, Texture, or Sprite registered with name: "
                                    + name);
            }
        }

        if (drawable instanceof BaseDrawable)
            ((BaseDrawable) drawable).setName(name);

        add(name, drawable, Drawable.class);
        return drawable;
    }

    /** Returns the name of the specified style object, or null if it is not in the skin. This compares potentially every style
     * object in the skin of the same type as the specified style, which may be a somewhat expensive operation. */
    public String find(Object resource) {
        if (resource == null)
            throw new IllegalArgumentException("style cannot be null.");
        ObjectMap<String, Object> typeResources = resources.get(resource.getClass());
        if (typeResources == null)
            return null;
        return typeResources.findKey(resource, true);
    }

    /** Returns a copy of a drawable found in the skin via {@link #getDrawable(String)}. */
    public Drawable newDrawable(String name) {
        return newDrawable(getDrawable(name));
    }

    /** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
    public Drawable newDrawable(String name, float r, float g, float b, float a) {
        return newDrawable(getDrawable(name), new Color(r, g, b, a));
    }

    /** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
    public Drawable newDrawable(String name, Color tint) {
        return newDrawable(getDrawable(name), tint);
    }

    /** Returns a copy of the specified drawable. */
    public Drawable newDrawable(Drawable drawable) {
        if (drawable instanceof TextureRegionDrawable)
            return new TextureRegionDrawable((TextureRegionDrawable) drawable);
        if (drawable instanceof NinePatchDrawable)
            return new NinePatchDrawable((NinePatchDrawable) drawable);
        if (drawable instanceof SpriteDrawable)
            return new SpriteDrawable((SpriteDrawable) drawable);
        throw new GdxRuntimeException("Unable to copy, unknown drawable type: " + drawable.getClass());
    }

    /** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
    public Drawable newDrawable(Drawable drawable, float r, float g, float b, float a) {
        return newDrawable(drawable, new Color(r, g, b, a));
    }

    /** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
    public Drawable newDrawable(Drawable drawable, Color tint) {
        Drawable newDrawable;
        if (drawable instanceof TextureRegionDrawable) {
            TextureRegion region = ((TextureRegionDrawable) drawable).getRegion();
            Sprite sprite;
            if (region instanceof AtlasRegion)
                sprite = new AtlasSprite((AtlasRegion) region);
            else
                sprite = new Sprite(region);
            sprite.setColor(tint);
            newDrawable = new SpriteDrawable(sprite);
        } else if (drawable instanceof NinePatchDrawable) {
            NinePatchDrawable patchDrawable = new NinePatchDrawable((NinePatchDrawable) drawable);
            patchDrawable.setPatch(new NinePatch(patchDrawable.getPatch(), tint));
            newDrawable = patchDrawable;
        } else if (drawable instanceof SpriteDrawable) {
            SpriteDrawable spriteDrawable = new SpriteDrawable((SpriteDrawable) drawable);
            Sprite sprite = spriteDrawable.getSprite();
            if (sprite instanceof AtlasSprite)
                sprite = new AtlasSprite((AtlasSprite) sprite);
            else
                sprite = new Sprite(sprite);
            sprite.setColor(tint);
            spriteDrawable.setSprite(sprite);
            newDrawable = spriteDrawable;
        } else
            throw new GdxRuntimeException("Unable to copy, unknown drawable type: " + drawable.getClass());

        if (newDrawable instanceof BaseDrawable) {
            BaseDrawable named = (BaseDrawable) newDrawable;
            if (drawable instanceof BaseDrawable)
                named.setName(((BaseDrawable) drawable).getName() + " (" + tint + ")");
            else
                named.setName(" (" + tint + ")");
        }

        return newDrawable;
    }

    /** Sets the style on the actor to disabled or enabled. This is done by appending "-disabled" to the style name when enabled is
     * false, and removing "-disabled" from the style name when enabled is true. A method named "getStyle" is called the actor via
     * reflection and the name of that style is found in the skin. If the actor doesn't have a "getStyle" method or the style was
     * not found in the skin, no exception is thrown and the actor is left unchanged. */
    public void setEnabled(Actor actor, boolean enabled) {
        // Get current style.
        Method method = findMethod(actor.getClass(), "getStyle");
        if (method == null)
            return;
        Object style;
        try {
            style = method.invoke(actor);
        } catch (Exception ignored) {
            return;
        }
        // Determine new style.
        String name = find(style);
        if (name == null)
            return;
        name = name.replace("-disabled", "") + (enabled ? "" : "-disabled");
        style = get(name, style.getClass());
        // Set new style.
        method = findMethod(actor.getClass(), "setStyle");
        if (method == null)
            return;
        try {
            method.invoke(actor, style);
        } catch (Exception ignored) {
        }
    }

    /** Returns the {@link TextureAtlas} that resources in this skin reference, or null. */
    public TextureAtlas getAtlas() {
        return atlas;
    }

    /** Disposes the {@link TextureAtlas} and all {@link Disposable} resources in the skin. */
    public void dispose() {
        if (atlas != null)
            atlas.dispose();
        for (ObjectMap<String, Object> entry : resources.values()) {
            for (Object resource : entry.values())
                if (resource instanceof Disposable)
                    ((Disposable) resource).dispose();
        }
    }

    protected Json getJsonLoader(final FileHandle skinFile) {
        final Skin skin = this;

        final Json json = new Json() {
            public <T> T readValue(Class<T> type, Class elementType, JsonValue jsonData) {
                // If the JSON is a string but the type is not, look up the actual value by name.
                if (jsonData.isString() && !ClassReflection.isAssignableFrom(CharSequence.class, type))
                    return get(jsonData.asString(), type);
                return super.readValue(type, elementType, jsonData);
            }
        };
        json.setTypeName(null);
        json.setUsePrototypes(false);

        json.setSerializer(Skin.class, new ReadOnlySerializer<Skin>() {
            public Skin read(Json json, JsonValue typeToValueMap, Class ignored) {
                for (JsonValue valueMap = typeToValueMap.child; valueMap != null; valueMap = valueMap.next) {
                    try {
                        readNamedObjects(json, ClassReflection.forName(valueMap.name()), valueMap);
                    } catch (ReflectionException ex) {
                        throw new SerializationException(ex);
                    }
                }
                return skin;
            }

            private void readNamedObjects(Json json, Class type, JsonValue valueMap) {
                Class addType = type == TintedDrawable.class ? Drawable.class : type;
                for (JsonValue valueEntry = valueMap.child; valueEntry != null; valueEntry = valueEntry.next) {
                    Object object = json.readValue(type, valueEntry);
                    if (object == null)
                        continue;
                    try {
                        add(valueEntry.name(), object, addType);
                    } catch (Exception ex) {
                        throw new SerializationException(
                                "Error reading " + ClassReflection.getSimpleName(type) + ": " + valueEntry.name(),
                                ex);
                    }
                }
            }
        });

        json.setSerializer(BitmapFont.class, new ReadOnlySerializer<BitmapFont>() {
            public BitmapFont read(Json json, JsonValue jsonData, Class type) {
                String path = json.readValue("file", String.class, jsonData);
                int scaledSize = json.readValue("scaledSize", int.class, -1, jsonData);
                Boolean flip = json.readValue("flip", Boolean.class, false, jsonData);

                FileHandle fontFile = skinFile.parent().child(path);
                if (!fontFile.exists())
                    fontFile = Gdx.files.internal(path);
                if (!fontFile.exists())
                    throw new SerializationException("Font file not found: " + fontFile);

                // Use a region with the same name as the font, else use a PNG file in the same directory as the FNT file.
                String regionName = fontFile.nameWithoutExtension();
                try {
                    BitmapFont font;
                    TextureRegion region = skin.optional(regionName, TextureRegion.class);
                    if (region != null)
                        font = new BitmapFont(fontFile, region, flip);
                    else {
                        FileHandle imageFile = fontFile.parent().child(regionName + ".png");
                        if (imageFile.exists())
                            font = new BitmapFont(fontFile, imageFile, flip);
                        else
                            font = new BitmapFont(fontFile, flip);
                    }
                    // Scaled size is the desired cap height to scale the font to.
                    if (scaledSize != -1)
                        font.setScale(scaledSize / font.getCapHeight());
                    return font;
                } catch (RuntimeException ex) {
                    throw new SerializationException("Error loading bitmap font: " + fontFile, ex);
                }
            }
        });

        json.setSerializer(Color.class, new ReadOnlySerializer<Color>() {
            public Color read(Json json, JsonValue jsonData, Class type) {
                if (jsonData.isString())
                    return get(jsonData.asString(), Color.class);
                String hex = json.readValue("hex", String.class, (String) null, jsonData);
                if (hex != null)
                    return Color.valueOf(hex);
                float r = json.readValue("r", float.class, 0f, jsonData);
                float g = json.readValue("g", float.class, 0f, jsonData);
                float b = json.readValue("b", float.class, 0f, jsonData);
                float a = json.readValue("a", float.class, 1f, jsonData);
                return new Color(r, g, b, a);
            }
        });

        json.setSerializer(TintedDrawable.class, new ReadOnlySerializer() {
            public Object read(Json json, JsonValue jsonData, Class type) {
                String name = json.readValue("name", String.class, jsonData);
                Color color = json.readValue("color", Color.class, jsonData);
                Drawable drawable = newDrawable(name, color);
                if (drawable instanceof BaseDrawable) {
                    BaseDrawable named = (BaseDrawable) drawable;
                    named.setName(jsonData.name + " (" + name + ", " + color + ")");
                }
                return drawable;
            }
        });

        return json;
    }

    static private Method findMethod(Class type, String name) {
        Method[] methods = ClassReflection.getMethods(type);
        for (int i = 0, n = methods.length; i < n; i++) {
            Method method = methods[i];
            if (method.getName().equals(name))
                return method;
        }
        return null;
    }

    /** @author Nathan Sweet */
    static public class TintedDrawable {
        public String name;
        public Color color;
    }
}