1/*******************************************************************************
2 * Copyright 2011 See AUTHORS file.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 ******************************************************************************/
16
17package com.badlogic.gdx.scenes.scene2d.ui;
18
19import com.badlogic.gdx.Gdx;
20import com.badlogic.gdx.files.FileHandle;
21import com.badlogic.gdx.graphics.Color;
22import com.badlogic.gdx.graphics.Texture;
23import com.badlogic.gdx.graphics.g2d.BitmapFont;
24import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
25import com.badlogic.gdx.graphics.g2d.NinePatch;
26import com.badlogic.gdx.graphics.g2d.Sprite;
27import com.badlogic.gdx.graphics.g2d.TextureAtlas;
28import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
29import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasSprite;
30import com.badlogic.gdx.graphics.g2d.TextureRegion;
31import com.badlogic.gdx.scenes.scene2d.Actor;
32import com.badlogic.gdx.scenes.scene2d.utils.BaseDrawable;
33import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
34import com.badlogic.gdx.scenes.scene2d.utils.NinePatchDrawable;
35import com.badlogic.gdx.scenes.scene2d.utils.SpriteDrawable;
36import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
37import com.badlogic.gdx.scenes.scene2d.utils.TiledDrawable;
38import com.badlogic.gdx.utils.Array;
39import com.badlogic.gdx.utils.Disposable;
40import com.badlogic.gdx.utils.GdxRuntimeException;
41import com.badlogic.gdx.utils.Json;
42import com.badlogic.gdx.utils.Json.ReadOnlySerializer;
43import com.badlogic.gdx.utils.JsonValue;
44import com.badlogic.gdx.utils.ObjectMap;
45import com.badlogic.gdx.utils.SerializationException;
46import com.badlogic.gdx.utils.reflect.ClassReflection;
47import com.badlogic.gdx.utils.reflect.Method;
48import com.badlogic.gdx.utils.reflect.ReflectionException;
49
50/** A skin stores resources for UI widgets to use (texture regions, ninepatches, fonts, colors, etc). Resources are named and can
51 * be looked up by name and type. Resources can be described in JSON. Skin provides useful conversions, such as allowing access to
52 * regions in the atlas as ninepatches, sprites, drawables, etc. The get* methods return an instance of the object in the skin.
53 * The new* methods return a copy of an instance in the skin.
54 * <p>
55 * See the <a href="https://github.com/libgdx/libgdx/wiki/Skin">documentation</a> for more.
56 * @author Nathan Sweet */
57public class Skin implements Disposable {
58	ObjectMap<Class, ObjectMap<String, Object>> resources = new ObjectMap();
59	TextureAtlas atlas;
60
61	/** Creates an empty skin. */
62	public Skin () {
63	}
64
65	/** Creates a skin containing the resources in the specified skin JSON file. If a file in the same directory with a ".atlas"
66	 * extension exists, it is loaded as a {@link TextureAtlas} and the texture regions added to the skin. The atlas is
67	 * automatically disposed when the skin is disposed. */
68	public Skin (FileHandle skinFile) {
69		FileHandle atlasFile = skinFile.sibling(skinFile.nameWithoutExtension() + ".atlas");
70		if (atlasFile.exists()) {
71			atlas = new TextureAtlas(atlasFile);
72			addRegions(atlas);
73		}
74
75		load(skinFile);
76	}
77
78	/** Creates a skin containing the resources in the specified skin JSON file and the texture regions from the specified atlas.
79	 * The atlas is automatically disposed when the skin is disposed. */
80	public Skin (FileHandle skinFile, TextureAtlas atlas) {
81		this.atlas = atlas;
82		addRegions(atlas);
83		load(skinFile);
84	}
85
86	/** Creates a skin containing the texture regions from the specified atlas. The atlas is automatically disposed when the skin
87	 * is disposed. */
88	public Skin (TextureAtlas atlas) {
89		this.atlas = atlas;
90		addRegions(atlas);
91	}
92
93	/** Adds all resources in the specified skin JSON file. */
94	public void load (FileHandle skinFile) {
95		try {
96			getJsonLoader(skinFile).fromJson(Skin.class, skinFile);
97		} catch (SerializationException ex) {
98			throw new SerializationException("Error reading file: " + skinFile, ex);
99		}
100	}
101
102	/** Adds all named texture regions from the atlas. The atlas will not be automatically disposed when the skin is disposed. */
103	public void addRegions (TextureAtlas atlas) {
104		Array<AtlasRegion> regions = atlas.getRegions();
105		for (int i = 0, n = regions.size; i < n; i++) {
106			AtlasRegion region = regions.get(i);
107			String name = region.name;
108			if (region.index != -1) {
109			    name += "_" + region.index;
110			}
111			add(name, region, TextureRegion.class);
112		}
113	}
114
115	public void add (String name, Object resource) {
116		add(name, resource, resource.getClass());
117	}
118
119	public void add (String name, Object resource, Class type) {
120		if (name == null) throw new IllegalArgumentException("name cannot be null.");
121		if (resource == null) throw new IllegalArgumentException("resource cannot be null.");
122		ObjectMap<String, Object> typeResources = resources.get(type);
123		if (typeResources == null) {
124			typeResources = new ObjectMap(type == TextureRegion.class || type == Drawable.class || type == Sprite.class ? 256 : 64);
125			resources.put(type, typeResources);
126		}
127		typeResources.put(name, resource);
128	}
129
130	public void remove (String name, Class type) {
131		if (name == null) throw new IllegalArgumentException("name cannot be null.");
132		ObjectMap<String, Object> typeResources = resources.get(type);
133		typeResources.remove(name);
134	}
135
136	/** Returns a resource named "default" for the specified type.
137	 * @throws GdxRuntimeException if the resource was not found. */
138	public <T> T get (Class<T> type) {
139		return get("default", type);
140	}
141
142	/** Returns a named resource of the specified type.
143	 * @throws GdxRuntimeException if the resource was not found. */
144	public <T> T get (String name, Class<T> type) {
145		if (name == null) throw new IllegalArgumentException("name cannot be null.");
146		if (type == null) throw new IllegalArgumentException("type cannot be null.");
147
148		if (type == Drawable.class) return (T)getDrawable(name);
149		if (type == TextureRegion.class) return (T)getRegion(name);
150		if (type == NinePatch.class) return (T)getPatch(name);
151		if (type == Sprite.class) return (T)getSprite(name);
152
153		ObjectMap<String, Object> typeResources = resources.get(type);
154		if (typeResources == null) throw new GdxRuntimeException("No " + type.getName() + " registered with name: " + name);
155		Object resource = typeResources.get(name);
156		if (resource == null) throw new GdxRuntimeException("No " + type.getName() + " registered with name: " + name);
157		return (T)resource;
158	}
159
160	/** Returns a named resource of the specified type.
161	 * @return null if not found. */
162	public <T> T optional (String name, Class<T> type) {
163		if (name == null) throw new IllegalArgumentException("name cannot be null.");
164		if (type == null) throw new IllegalArgumentException("type cannot be null.");
165		ObjectMap<String, Object> typeResources = resources.get(type);
166		if (typeResources == null) return null;
167		return (T)typeResources.get(name);
168	}
169
170	public boolean has (String name, Class type) {
171		ObjectMap<String, Object> typeResources = resources.get(type);
172		if (typeResources == null) return false;
173		return typeResources.containsKey(name);
174	}
175
176	/** Returns the name to resource mapping for the specified type, or null if no resources of that type exist. */
177	public <T> ObjectMap<String, T> getAll (Class<T> type) {
178		return (ObjectMap<String, T>)resources.get(type);
179	}
180
181	public Color getColor (String name) {
182		return get(name, Color.class);
183	}
184
185	public BitmapFont getFont (String name) {
186		return get(name, BitmapFont.class);
187	}
188
189	/** Returns a registered texture region. If no region is found but a texture exists with the name, a region is created from the
190	 * texture and stored in the skin. */
191	public TextureRegion getRegion (String name) {
192		TextureRegion region = optional(name, TextureRegion.class);
193		if (region != null) return region;
194
195		Texture texture = optional(name, Texture.class);
196		if (texture == null) throw new GdxRuntimeException("No TextureRegion or Texture registered with name: " + name);
197		region = new TextureRegion(texture);
198		add(name, region, TextureRegion.class);
199		return region;
200	}
201
202	/** @return an array with the {@link TextureRegion} that have an index != -1, or null if none are found. */
203	public Array<TextureRegion> getRegions (String regionName) {
204		Array<TextureRegion> regions = null;
205		int i = 0;
206		TextureRegion region = optional(regionName + "_" + (i++), TextureRegion.class);
207		if (region != null) {
208			regions = new Array<TextureRegion>();
209			while (region != null) {
210				regions.add(region);
211				region = optional(regionName + "_" + (i++), TextureRegion.class);
212			}
213		}
214		return regions;
215	}
216
217	/** Returns a registered tiled drawable. If no tiled drawable is found but a region exists with the name, a tiled drawable is
218	 * created from the region and stored in the skin. */
219	public TiledDrawable getTiledDrawable (String name) {
220		TiledDrawable tiled = optional(name, TiledDrawable.class);
221		if (tiled != null) return tiled;
222
223		tiled = new TiledDrawable(getRegion(name));
224		tiled.setName(name);
225		add(name, tiled, TiledDrawable.class);
226		return tiled;
227	}
228
229	/** Returns a registered ninepatch. If no ninepatch is found but a region exists with the name, a ninepatch is created from the
230	 * region and stored in the skin. If the region is an {@link AtlasRegion} then the {@link AtlasRegion#splits} are used,
231	 * otherwise the ninepatch will have the region as the center patch. */
232	public NinePatch getPatch (String name) {
233		NinePatch patch = optional(name, NinePatch.class);
234		if (patch != null) return patch;
235
236		try {
237			TextureRegion region = getRegion(name);
238			if (region instanceof AtlasRegion) {
239				int[] splits = ((AtlasRegion)region).splits;
240				if (splits != null) {
241					patch = new NinePatch(region, splits[0], splits[1], splits[2], splits[3]);
242					int[] pads = ((AtlasRegion)region).pads;
243					if (pads != null) patch.setPadding(pads[0], pads[1], pads[2], pads[3]);
244				}
245			}
246			if (patch == null) patch = new NinePatch(region);
247			add(name, patch, NinePatch.class);
248			return patch;
249		} catch (GdxRuntimeException ex) {
250			throw new GdxRuntimeException("No NinePatch, TextureRegion, or Texture registered with name: " + name);
251		}
252	}
253
254	/** Returns a registered sprite. If no sprite is found but a region exists with the name, a sprite is created from the region
255	 * and stored in the skin. If the region is an {@link AtlasRegion} then an {@link AtlasSprite} is used if the region has been
256	 * whitespace stripped or packed rotated 90 degrees. */
257	public Sprite getSprite (String name) {
258		Sprite sprite = optional(name, Sprite.class);
259		if (sprite != null) return sprite;
260
261		try {
262			TextureRegion textureRegion = getRegion(name);
263			if (textureRegion instanceof AtlasRegion) {
264				AtlasRegion region = (AtlasRegion)textureRegion;
265				if (region.rotate || region.packedWidth != region.originalWidth || region.packedHeight != region.originalHeight)
266					sprite = new AtlasSprite(region);
267			}
268			if (sprite == null) sprite = new Sprite(textureRegion);
269			add(name, sprite, Sprite.class);
270			return sprite;
271		} catch (GdxRuntimeException ex) {
272			throw new GdxRuntimeException("No NinePatch, TextureRegion, or Texture registered with name: " + name);
273		}
274	}
275
276	/** Returns a registered drawable. If no drawable is found but a region, ninepatch, or sprite exists with the name, then the
277	 * appropriate drawable is created and stored in the skin. */
278	public Drawable getDrawable (String name) {
279		Drawable drawable = optional(name, Drawable.class);
280		if (drawable != null) return drawable;
281
282		// Use texture or texture region. If it has splits, use ninepatch. If it has rotation or whitespace stripping, use sprite.
283		try {
284			TextureRegion textureRegion = getRegion(name);
285			if (textureRegion instanceof AtlasRegion) {
286				AtlasRegion region = (AtlasRegion)textureRegion;
287				if (region.splits != null)
288					drawable = new NinePatchDrawable(getPatch(name));
289				else if (region.rotate || region.packedWidth != region.originalWidth || region.packedHeight != region.originalHeight)
290					drawable = new SpriteDrawable(getSprite(name));
291			}
292			if (drawable == null) drawable = new TextureRegionDrawable(textureRegion);
293		} catch (GdxRuntimeException ignored) {
294		}
295
296		// Check for explicit registration of ninepatch, sprite, or tiled drawable.
297		if (drawable == null) {
298			NinePatch patch = optional(name, NinePatch.class);
299			if (patch != null)
300				drawable = new NinePatchDrawable(patch);
301			else {
302				Sprite sprite = optional(name, Sprite.class);
303				if (sprite != null)
304					drawable = new SpriteDrawable(sprite);
305				else
306					throw new GdxRuntimeException(
307						"No Drawable, NinePatch, TextureRegion, Texture, or Sprite registered with name: " + name);
308			}
309		}
310
311		if (drawable instanceof BaseDrawable) ((BaseDrawable)drawable).setName(name);
312
313		add(name, drawable, Drawable.class);
314		return drawable;
315	}
316
317	/** Returns the name of the specified style object, or null if it is not in the skin. This compares potentially every style
318	 * object in the skin of the same type as the specified style, which may be a somewhat expensive operation. */
319	public String find (Object resource) {
320		if (resource == null) throw new IllegalArgumentException("style cannot be null.");
321		ObjectMap<String, Object> typeResources = resources.get(resource.getClass());
322		if (typeResources == null) return null;
323		return typeResources.findKey(resource, true);
324	}
325
326	/** Returns a copy of a drawable found in the skin via {@link #getDrawable(String)}. */
327	public Drawable newDrawable (String name) {
328		return newDrawable(getDrawable(name));
329	}
330
331	/** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
332	public Drawable newDrawable (String name, float r, float g, float b, float a) {
333		return newDrawable(getDrawable(name), new Color(r, g, b, a));
334	}
335
336	/** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
337	public Drawable newDrawable (String name, Color tint) {
338		return newDrawable(getDrawable(name), tint);
339	}
340
341	/** Returns a copy of the specified drawable. */
342	public Drawable newDrawable (Drawable drawable) {
343		if (drawable instanceof TiledDrawable) return new TiledDrawable((TiledDrawable)drawable);
344		if (drawable instanceof TextureRegionDrawable) return new TextureRegionDrawable((TextureRegionDrawable)drawable);
345		if (drawable instanceof NinePatchDrawable) return new NinePatchDrawable((NinePatchDrawable)drawable);
346		if (drawable instanceof SpriteDrawable) return new SpriteDrawable((SpriteDrawable)drawable);
347		throw new GdxRuntimeException("Unable to copy, unknown drawable type: " + drawable.getClass());
348	}
349
350	/** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
351	public Drawable newDrawable (Drawable drawable, float r, float g, float b, float a) {
352		return newDrawable(drawable, new Color(r, g, b, a));
353	}
354
355	/** Returns a tinted copy of a drawable found in the skin via {@link #getDrawable(String)}. */
356	public Drawable newDrawable (Drawable drawable, Color tint) {
357		Drawable newDrawable;
358		if (drawable instanceof TextureRegionDrawable)
359			newDrawable = ((TextureRegionDrawable)drawable).tint(tint);
360		else if (drawable instanceof NinePatchDrawable)
361			newDrawable = ((NinePatchDrawable)drawable).tint(tint);
362		else if (drawable instanceof SpriteDrawable)
363			newDrawable = ((SpriteDrawable)drawable).tint(tint);
364		else
365			throw new GdxRuntimeException("Unable to copy, unknown drawable type: " + drawable.getClass());
366
367		if (newDrawable instanceof BaseDrawable) {
368			BaseDrawable named = (BaseDrawable)newDrawable;
369			if (drawable instanceof BaseDrawable)
370				named.setName(((BaseDrawable)drawable).getName() + " (" + tint + ")");
371			else
372				named.setName(" (" + tint + ")");
373		}
374
375		return newDrawable;
376	}
377
378	/** Sets the style on the actor to disabled or enabled. This is done by appending "-disabled" to the style name when enabled is
379	 * false, and removing "-disabled" from the style name when enabled is true. A method named "getStyle" is called the actor via
380	 * 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
381	 * not found in the skin, no exception is thrown and the actor is left unchanged. */
382	public void setEnabled (Actor actor, boolean enabled) {
383		// Get current style.
384		Method method = findMethod(actor.getClass(), "getStyle");
385		if (method == null) return;
386		Object style;
387		try {
388			style = method.invoke(actor);
389		} catch (Exception ignored) {
390			return;
391		}
392		// Determine new style.
393		String name = find(style);
394		if (name == null) return;
395		name = name.replace("-disabled", "") + (enabled ? "" : "-disabled");
396		style = get(name, style.getClass());
397		// Set new style.
398		method = findMethod(actor.getClass(), "setStyle");
399		if (method == null) return;
400		try {
401			method.invoke(actor, style);
402		} catch (Exception ignored) {
403		}
404	}
405
406	/** Returns the {@link TextureAtlas} passed to this skin constructor, or null. */
407	public TextureAtlas getAtlas () {
408		return atlas;
409	}
410
411	/** Disposes the {@link TextureAtlas} and all {@link Disposable} resources in the skin. */
412	public void dispose () {
413		if (atlas != null) atlas.dispose();
414		for (ObjectMap<String, Object> entry : resources.values()) {
415			for (Object resource : entry.values())
416				if (resource instanceof Disposable) ((Disposable)resource).dispose();
417		}
418	}
419
420	protected Json getJsonLoader (final FileHandle skinFile) {
421		final Skin skin = this;
422
423		final Json json = new Json() {
424			public <T> T readValue (Class<T> type, Class elementType, JsonValue jsonData) {
425				// If the JSON is a string but the type is not, look up the actual value by name.
426				if (jsonData.isString() && !ClassReflection.isAssignableFrom(CharSequence.class, type))
427					return get(jsonData.asString(), type);
428				return super.readValue(type, elementType, jsonData);
429			}
430		};
431		json.setTypeName(null);
432		json.setUsePrototypes(false);
433
434		json.setSerializer(Skin.class, new ReadOnlySerializer<Skin>() {
435			public Skin read (Json json, JsonValue typeToValueMap, Class ignored) {
436				for (JsonValue valueMap = typeToValueMap.child; valueMap != null; valueMap = valueMap.next) {
437					try {
438						readNamedObjects(json, ClassReflection.forName(valueMap.name()), valueMap);
439					} catch (ReflectionException ex) {
440						throw new SerializationException(ex);
441					}
442				}
443				return skin;
444			}
445
446			private void readNamedObjects (Json json, Class type, JsonValue valueMap) {
447				Class addType = type == TintedDrawable.class ? Drawable.class : type;
448				for (JsonValue valueEntry = valueMap.child; valueEntry != null; valueEntry = valueEntry.next) {
449					Object object = json.readValue(type, valueEntry);
450					if (object == null) continue;
451					try {
452						add(valueEntry.name, object, addType);
453						if (addType != Drawable.class && ClassReflection.isAssignableFrom(Drawable.class, addType))
454							add(valueEntry.name, object, Drawable.class);
455					} catch (Exception ex) {
456						throw new SerializationException(
457							"Error reading " + ClassReflection.getSimpleName(type) + ": " + valueEntry.name, ex);
458					}
459				}
460			}
461		});
462
463		json.setSerializer(BitmapFont.class, new ReadOnlySerializer<BitmapFont>() {
464			public BitmapFont read (Json json, JsonValue jsonData, Class type) {
465				String path = json.readValue("file", String.class, jsonData);
466				int scaledSize = json.readValue("scaledSize", int.class, -1, jsonData);
467				Boolean flip = json.readValue("flip", Boolean.class, false, jsonData);
468				Boolean markupEnabled = json.readValue("markupEnabled", Boolean.class, false, jsonData);
469
470				FileHandle fontFile = skinFile.parent().child(path);
471				if (!fontFile.exists()) fontFile = Gdx.files.internal(path);
472				if (!fontFile.exists()) throw new SerializationException("Font file not found: " + fontFile);
473
474				// Use a region with the same name as the font, else use a PNG file in the same directory as the FNT file.
475				String regionName = fontFile.nameWithoutExtension();
476				try {
477					BitmapFont font;
478					Array<TextureRegion> regions = skin.getRegions(regionName);
479					if (regions != null)
480						font = new BitmapFont(new BitmapFontData(fontFile, flip), regions, true);
481					else {
482						TextureRegion region = skin.optional(regionName, TextureRegion.class);
483						if (region != null)
484							font = new BitmapFont(fontFile, region, flip);
485						else {
486							FileHandle imageFile = fontFile.parent().child(regionName + ".png");
487							if (imageFile.exists())
488								font = new BitmapFont(fontFile, imageFile, flip);
489							else
490								font = new BitmapFont(fontFile, flip);
491						}
492					}
493					font.getData().markupEnabled = markupEnabled;
494					// Scaled size is the desired cap height to scale the font to.
495					if (scaledSize != -1) font.getData().setScale(scaledSize / font.getCapHeight());
496					return font;
497				} catch (RuntimeException ex) {
498					throw new SerializationException("Error loading bitmap font: " + fontFile, ex);
499				}
500			}
501		});
502
503		json.setSerializer(Color.class, new ReadOnlySerializer<Color>() {
504			public Color read (Json json, JsonValue jsonData, Class type) {
505				if (jsonData.isString()) return get(jsonData.asString(), Color.class);
506				String hex = json.readValue("hex", String.class, (String)null, jsonData);
507				if (hex != null) return Color.valueOf(hex);
508				float r = json.readValue("r", float.class, 0f, jsonData);
509				float g = json.readValue("g", float.class, 0f, jsonData);
510				float b = json.readValue("b", float.class, 0f, jsonData);
511				float a = json.readValue("a", float.class, 1f, jsonData);
512				return new Color(r, g, b, a);
513			}
514		});
515
516		json.setSerializer(TintedDrawable.class, new ReadOnlySerializer() {
517			public Object read (Json json, JsonValue jsonData, Class type) {
518				String name = json.readValue("name", String.class, jsonData);
519				Color color = json.readValue("color", Color.class, jsonData);
520				Drawable drawable = newDrawable(name, color);
521				if (drawable instanceof BaseDrawable) {
522					BaseDrawable named = (BaseDrawable)drawable;
523					named.setName(jsonData.name + " (" + name + ", " + color + ")");
524				}
525				return drawable;
526			}
527		});
528
529		return json;
530	}
531
532	static private Method findMethod (Class type, String name) {
533		Method[] methods = ClassReflection.getMethods(type);
534		for (int i = 0, n = methods.length; i < n; i++) {
535			Method method = methods[i];
536			if (method.getName().equals(name)) return method;
537		}
538		return null;
539	}
540
541	/** @author Nathan Sweet */
542	static public class TintedDrawable {
543		public String name;
544		public Color color;
545	}
546}
547