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