瀏覽代碼

Merge branch '4.0-beta' of https://github.com/esotericsoftware/spine-runtimes into 4.0-beta

badlogic 4 年之前
父節點
當前提交
3943bdd9b0

+ 107 - 0
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewAtlas.java

@@ -0,0 +1,107 @@
+
+package com.esotericsoftware.spine;
+
+import static com.esotericsoftware.spine.SkeletonViewer.*;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.Pixmap;
+import com.badlogic.gdx.graphics.Pixmap.Format;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.Texture.TextureFilter;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData.Page;
+import com.badlogic.gdx.utils.Null;
+
+class SkeletonViewAtlas extends TextureAtlas {
+	private final SkeletonViewer viewer;
+	private @Null FileHandle atlasFile;
+	private final AtlasRegion fake;
+
+	public SkeletonViewAtlas (final SkeletonViewer viewer, @Null FileHandle skeletonFile) {
+		this.viewer = viewer;
+
+		atlasFile = findAtlasFile(skeletonFile);
+		if (atlasFile != null) {
+			final TextureAtlasData atlasData = new TextureAtlasData(atlasFile, atlasFile.parent(), false);
+			Gdx.app.postRunnable(new Runnable() {
+				public void run () {
+					boolean linear = true, pma = false;
+					for (int i = 0, n = atlasData.getPages().size; i < n; i++) {
+						Page page = atlasData.getPages().get(i);
+						if (page.pma) pma = true;
+						if (page.minFilter != TextureFilter.Linear || page.magFilter != TextureFilter.Linear) {
+							linear = false;
+							break;
+						}
+					}
+					viewer.ui.linearCheckbox.setChecked(linear);
+					viewer.ui.pmaCheckbox.setChecked(pma);
+				}
+			});
+			try {
+				load(atlasData);
+			} catch (Throwable ex) {
+				System.out.println("Error loading atlas: " + atlasFile.file().getAbsolutePath());
+				ex.printStackTrace();
+				viewer.ui.toast("Error loading atlas: " + atlasFile.name());
+				atlasFile = null;
+			}
+		}
+
+		// Setup a texture atlas that uses a white image for images not found in the atlas.
+		Pixmap pixmap = new Pixmap(32, 32, Format.RGBA8888);
+		pixmap.setColor(new Color(1, 1, 1, 0.33f));
+		pixmap.fill();
+		fake = new AtlasRegion(new Texture(pixmap), 0, 0, 32, 32);
+		pixmap.dispose();
+	}
+
+	private @Null FileHandle findAtlasFile (FileHandle skeletonFile) {
+		String baseName = skeletonFile.name();
+		for (String startSuffix : startSuffixes) {
+			for (String endSuffix : endSuffixes) {
+				for (String dataSuffix : dataSuffixes) {
+					String suffix = startSuffix + dataSuffix + endSuffix;
+					if (baseName.endsWith(suffix)) {
+						FileHandle file = findAtlasFile(skeletonFile, baseName.substring(0, baseName.length() - suffix.length()));
+						if (file != null) return file;
+					}
+				}
+			}
+		}
+		return findAtlasFile(skeletonFile, baseName);
+	}
+
+	private @Null FileHandle findAtlasFile (FileHandle skeletonFile, String baseName) {
+		for (String startSuffix : startSuffixes) {
+			for (String endSuffix : endSuffixes) {
+				for (String suffix : atlasSuffixes) {
+					FileHandle file = skeletonFile.sibling(baseName + startSuffix + suffix + endSuffix);
+					if (file.exists()) return file;
+				}
+			}
+		}
+		return null;
+	}
+
+	public AtlasRegion findRegion (String name) {
+		AtlasRegion region = super.findRegion(name);
+		if (region == null) {
+			// Look for separate image file.
+			FileHandle file = viewer.skeletonFile.sibling(name + ".png");
+			if (file.exists()) {
+				Texture texture = new Texture(file);
+				texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
+				region = new AtlasRegion(texture, 0, 0, texture.getWidth(), texture.getHeight());
+				region.name = name;
+			}
+		}
+		return region != null ? region : fake;
+	}
+
+	public long lastModified () {
+		return atlasFile == null ? 0 : atlasFile.lastModified();
+	}
+}

+ 30 - 805
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

@@ -29,17 +29,11 @@
 
 package com.esotericsoftware.spine;
 
-import static com.badlogic.gdx.math.Interpolation.*;
-import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
-
-import java.io.File;
 import java.lang.Thread.UncaughtExceptionHandler;
 import java.lang.reflect.Field;
 
 import com.badlogic.gdx.ApplicationAdapter;
 import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.InputAdapter;
-import com.badlogic.gdx.InputMultiplexer;
 import com.badlogic.gdx.Preferences;
 import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
 import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
@@ -47,36 +41,8 @@ import com.badlogic.gdx.files.FileHandle;
 import com.badlogic.gdx.graphics.Color;
 import com.badlogic.gdx.graphics.GL20;
 import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.Pixmap;
-import com.badlogic.gdx.graphics.Pixmap.Format;
-import com.badlogic.gdx.graphics.Texture;
-import com.badlogic.gdx.graphics.Texture.TextureFilter;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData.Page;
 import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
 import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
-import com.badlogic.gdx.math.MathUtils;
-import com.badlogic.gdx.scenes.scene2d.Actor;
-import com.badlogic.gdx.scenes.scene2d.InputEvent;
-import com.badlogic.gdx.scenes.scene2d.InputListener;
-import com.badlogic.gdx.scenes.scene2d.Stage;
-import com.badlogic.gdx.scenes.scene2d.Touchable;
-import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup;
-import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
-import com.badlogic.gdx.scenes.scene2d.ui.Image;
-import com.badlogic.gdx.scenes.scene2d.ui.Label;
-import com.badlogic.gdx.scenes.scene2d.ui.List;
-import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
-import com.badlogic.gdx.scenes.scene2d.ui.Slider;
-import com.badlogic.gdx.scenes.scene2d.ui.Table;
-import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
-import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup;
-import com.badlogic.gdx.scenes.scene2d.ui.Window;
-import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
-import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
-import com.badlogic.gdx.utils.Align;
 import com.badlogic.gdx.utils.Array;
 import com.badlogic.gdx.utils.Null;
 import com.badlogic.gdx.utils.StringBuilder;
@@ -87,60 +53,58 @@ import com.esotericsoftware.spine.AnimationState.AnimationStateAdapter;
 import com.esotericsoftware.spine.AnimationState.TrackEntry;
 import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
 
-import java.awt.FileDialog;
-import java.awt.Frame;
 import java.awt.Toolkit;
 
 public class SkeletonViewer extends ApplicationAdapter {
+	static final String version = ""; // Replaced by build.
 	static final float checkModifiedInterval = 0.250f;
 	static final float reloadDelay = 1;
-	static float uiScale = 1;
-	static String[] startSuffixes = {"", "-pro", "-ess"};
-	static String[] dataSuffixes = {".json", ".skel"};
-	static String[] endSuffixes = {"", ".txt", ".bytes"};
-	static String[] atlasSuffixes = {".atlas", "-pma.atlas"};
+	static final String[] startSuffixes = {"", "-pro", "-ess"};
+	static final String[] dataSuffixes = {".json", ".skel"};
+	static final String[] endSuffixes = {"", ".txt", ".bytes"};
+	static final String[] atlasSuffixes = {".atlas", "-pma.atlas"};
 	static String[] args;
-	static final String version = ""; // Replaced by build.
-
-	UI ui;
+	static float uiScale = 1;
 
-	OrthographicCamera camera;
+	Preferences prefs;
 	TwoColorPolygonBatch batch;
-	TextureAtlas atlas;
+	OrthographicCamera camera;
 	SkeletonRenderer renderer;
 	SkeletonRendererDebug debugRenderer;
+	SkeletonViewerUI ui;
+
+	SkeletonViewAtlas atlas;
 	SkeletonData skeletonData;
 	Skeleton skeleton;
 	AnimationState state;
 	FileHandle skeletonFile;
 	long skeletonModified, atlasModified;
 	float lastModifiedCheck, reloadTimer;
-	StringBuilder status = new StringBuilder();
-	Preferences prefs;
+	final StringBuilder status = new StringBuilder();
 
 	public void create () {
 		Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
 			public void uncaughtException (Thread thread, Throwable ex) {
+				System.out.println("Uncaught exception:");
 				ex.printStackTrace();
 				Runtime.getRuntime().halt(0); // Prevent Swing from keeping JVM alive.
 			}
 		});
 
 		prefs = Gdx.app.getPreferences("spine-skeletonviewer");
-		ui = new UI();
 		batch = new TwoColorPolygonBatch(3100);
 		camera = new OrthographicCamera();
 		renderer = new SkeletonRenderer();
 		debugRenderer = new SkeletonRendererDebug();
+		ui = new SkeletonViewerUI(this);
 		resetCameraPosition();
 		ui.loadPrefs();
 
 		if (args.length == 0) {
 			loadSkeleton(
 				Gdx.files.internal(Gdx.app.getPreferences("spine-skeletonviewer").getString("lastFile", "spineboy/spineboy.json")));
-		} else {
+		} else
 			loadSkeleton(Gdx.files.internal(args[0]));
-		}
 
 		ui.loadPrefs();
 		ui.prefsLoaded = true;
@@ -152,90 +116,24 @@ public class SkeletonViewer extends ApplicationAdapter {
 		}
 	}
 
-	FileHandle atlasFile (FileHandle skeletonFile) {
-		String baseName = skeletonFile.name();
-		for (String startSuffix : startSuffixes) {
-			for (String endSuffix : endSuffixes) {
-				for (String dataSuffix : dataSuffixes) {
-					String suffix = startSuffix + dataSuffix + endSuffix;
-					if (baseName.endsWith(suffix)) {
-						FileHandle file = atlasFile(skeletonFile, baseName.substring(0, baseName.length() - suffix.length()));
-						if (file != null) return file;
-					}
-				}
-			}
-		}
-		return atlasFile(skeletonFile, baseName);
-	}
-
-	private FileHandle atlasFile (FileHandle skeletonFile, String baseName) {
-		for (String startSuffix : startSuffixes) {
-			for (String endSuffix : endSuffixes) {
-				for (String suffix : atlasSuffixes) {
-					FileHandle file = skeletonFile.sibling(baseName + startSuffix + suffix + endSuffix);
-					if (file.exists()) return file;
-				}
-			}
-		}
-		return null;
-	}
-
-	void loadSkeleton (final FileHandle skeletonFile) {
+	void loadSkeleton (final @Null FileHandle skeletonFile) {
 		if (skeletonFile == null) return;
 
-		FileHandle atlasFile = atlasFile(skeletonFile);
 		try {
-			// Setup a texture atlas that uses a white image for images not found in the atlas.
-			Pixmap pixmap = new Pixmap(32, 32, Format.RGBA8888);
-			pixmap.setColor(new Color(1, 1, 1, 0.33f));
-			pixmap.fill();
-			final AtlasRegion fake = new AtlasRegion(new Texture(pixmap), 0, 0, 32, 32);
-			pixmap.dispose();
-
-			TextureAtlasData atlasData = null;
-			if (atlasFile != null) {
-				atlasData = new TextureAtlasData(atlasFile, atlasFile.parent(), false);
-				boolean linear = true;
-				for (int i = 0, n = atlasData.getPages().size; i < n; i++) {
-					Page page = atlasData.getPages().get(i);
-					if (page.minFilter != TextureFilter.Linear || page.magFilter != TextureFilter.Linear) {
-						linear = false;
-						break;
-					}
-				}
-				ui.linearCheckbox.setChecked(linear);
-			}
-
-			atlas = new TextureAtlas(atlasData) {
-				public AtlasRegion findRegion (String name) {
-					AtlasRegion region = super.findRegion(name);
-					if (region == null) {
-						// Look for separate image file.
-						FileHandle file = skeletonFile.sibling(name + ".png");
-						if (file.exists()) {
-							Texture texture = new Texture(file);
-							texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
-							region = new AtlasRegion(texture, 0, 0, texture.getWidth(), texture.getHeight());
-							region.name = name;
-						}
-					}
-					return region != null ? region : fake;
-				}
-			};
+			atlas = new SkeletonViewAtlas(this, skeletonFile);
 
 			// Load skeleton data.
 			String extension = skeletonFile.extension();
-			if (extension.equalsIgnoreCase("json") || extension.equalsIgnoreCase("txt")) {
-				SkeletonJson json = new SkeletonJson(atlas);
-				json.setScale(ui.loadScaleSlider.getValue());
-				skeletonData = json.readSkeletonData(skeletonFile);
-			} else {
-				SkeletonBinary binary = new SkeletonBinary(atlas);
-				binary.setScale(ui.loadScaleSlider.getValue());
-				skeletonData = binary.readSkeletonData(skeletonFile);
-				if (skeletonData.getBones().size == 0) throw new Exception("No bones in skeleton data.");
-			}
+			SkeletonLoader loader;
+			if (extension.equalsIgnoreCase("json") || extension.equalsIgnoreCase("txt"))
+				loader = new SkeletonJson(atlas);
+			else
+				loader = new SkeletonBinary(atlas);
+			loader.setScale(ui.loadScaleSlider.getValue());
+			skeletonData = loader.readSkeletonData(skeletonFile);
+			if (skeletonData.getBones().size == 0) throw new Exception("No bones in skeleton data.");
 		} catch (Throwable ex) {
+			System.out.println("Error loading skeleton: " + skeletonFile.file().getAbsolutePath());
 			ex.printStackTrace();
 			ui.toast("Error loading skeleton: " + skeletonFile.name());
 			lastModifiedCheck = 5;
@@ -258,7 +156,7 @@ public class SkeletonViewer extends ApplicationAdapter {
 
 		this.skeletonFile = skeletonFile;
 		skeletonModified = skeletonFile.lastModified();
-		atlasModified = atlasFile == null ? 0 : atlasFile.lastModified();
+		atlasModified = atlas.lastModified();
 		lastModifiedCheck = checkModifiedInterval;
 		prefs.putString("lastFile", skeletonFile.path());
 		prefs.flush();
@@ -332,8 +230,7 @@ public class SkeletonViewer extends ApplicationAdapter {
 					lastModifiedCheck = checkModifiedInterval;
 					long time = skeletonFile.lastModified();
 					if (time != 0 && skeletonModified != time) reloadTimer = reloadDelay;
-					FileHandle atlasFile = atlasFile(skeletonFile);
-					time = atlasFile == null ? 0 : atlasFile.lastModified();
+					time = atlas.lastModified();
 					if (time != 0 && atlasModified != time) reloadTimer = reloadDelay;
 				}
 			} else {
@@ -346,8 +243,8 @@ public class SkeletonViewer extends ApplicationAdapter {
 
 			// Pose and render skeleton.
 			state.getData().setDefaultMix(ui.mixSlider.getValue());
-			renderer.setPremultipliedAlpha(ui.premultipliedCheckbox.isChecked());
-			batch.setPremultipliedAlpha(ui.premultipliedCheckbox.isChecked());
+			renderer.setPremultipliedAlpha(ui.pmaCheckbox.isChecked());
+			batch.setPremultipliedAlpha(ui.pmaCheckbox.isChecked());
 
 			float scaleX = ui.xScaleSlider.getValue(), scaleY = ui.yScaleSlider.getValue();
 			if (skeleton.scaleX == 0) skeleton.scaleX = 0.01f;
@@ -441,678 +338,6 @@ public class SkeletonViewer extends ApplicationAdapter {
 		if (!ui.minimizeButton.isChecked()) ui.window.setHeight(height / uiScale + 8);
 	}
 
-	class UI {
-		boolean prefsLoaded;
-
-		Stage stage = new Stage(new ScreenViewport());
-		com.badlogic.gdx.scenes.scene2d.ui.Skin skin = new com.badlogic.gdx.scenes.scene2d.ui.Skin(
-			Gdx.files.internal("skin/skin.json"));
-
-		Window window = new Window("Skeleton", skin);
-		Table root = new Table(skin);
-		TextButton openButton = new TextButton("Open", skin);
-		TextButton minimizeButton = new TextButton("-", skin);
-
-		Slider loadScaleSlider = new Slider(0.1f, 3, 0.01f, false, skin);
-		Label loadScaleLabel = new Label("100%", skin);
-		TextButton loadScaleResetButton = new TextButton("Reload", skin);
-
-		Slider zoomSlider = new Slider(0.01f, 10, 0.01f, false, skin);
-		Label zoomLabel = new Label("100%", skin);
-		TextButton zoomResetButton = new TextButton("Reset", skin);
-
-		Slider xScaleSlider = new Slider(-2, 2, 0.01f, false, skin);
-		Label xScaleLabel = new Label("100%", skin);
-		TextButton xScaleResetButton = new TextButton("Reset", skin);
-
-		Slider yScaleSlider = new Slider(-2, 2, 0.01f, false, skin);
-		Label yScaleLabel = new Label("100%", skin);
-		TextButton yScaleResetButton = new TextButton("Reset", skin);
-
-		CheckBox debugBonesCheckbox = new CheckBox("Bones", skin);
-		CheckBox debugRegionsCheckbox = new CheckBox("Regions", skin);
-		CheckBox debugBoundingBoxesCheckbox = new CheckBox("Bounds", skin);
-		CheckBox debugMeshHullCheckbox = new CheckBox("Mesh hull", skin);
-		CheckBox debugMeshTrianglesCheckbox = new CheckBox("Triangles", skin);
-		CheckBox debugPathsCheckbox = new CheckBox("Paths", skin);
-		CheckBox debugPointsCheckbox = new CheckBox("Points", skin);
-		CheckBox debugClippingCheckbox = new CheckBox("Clipping", skin);
-
-		CheckBox premultipliedCheckbox = new CheckBox("Premultiplied", skin);
-
-		CheckBox linearCheckbox = new CheckBox("Linear", skin);
-
-		TextButton bonesSetupPoseButton = new TextButton("Bones", skin);
-		TextButton slotsSetupPoseButton = new TextButton("Slots", skin);
-		TextButton setupPoseButton = new TextButton("Both", skin);
-
-		List<String> skinList = new List(skin);
-		ScrollPane skinScroll = new ScrollPane(skinList, skin, "bg");
-
-		ButtonGroup<TextButton> trackButtons = new ButtonGroup();
-		CheckBox loopCheckbox = new CheckBox("Loop", skin);
-		CheckBox addCheckbox = new CheckBox("Add", skin);
-
-		Slider alphaSlider = new Slider(0, 1, 0.01f, false, skin);
-		Label alphaLabel = new Label("100%", skin);
-
-		List<String> animationList = new List(skin);
-		ScrollPane animationScroll = new ScrollPane(animationList, skin, "bg");
-
-		Slider speedSlider = new Slider(0, 3, 0.01f, false, skin);
-		Label speedLabel = new Label("1.0x", skin);
-		TextButton speedResetButton = new TextButton("Reset", skin);
-
-		CheckBox reverseCheckbox = new CheckBox("Reverse", skin);
-		CheckBox holdPrevCheckbox = new CheckBox("Hold previous", skin);
-
-		Slider mixSlider = new Slider(0, 4, 0.01f, false, skin);
-		Label mixLabel = new Label("0.3s", skin);
-
-		Label statusLabel = new Label("", skin);
-		WidgetGroup toasts = new WidgetGroup();
-
-		UI () {
-			initialize();
-			layout();
-			events();
-		}
-
-		void initialize () {
-			skin.getFont("default").getData().markupEnabled = true;
-
-			for (int i = 0; i < 6; i++)
-				trackButtons.add(new TextButton(i + "", skin, "toggle"));
-
-			premultipliedCheckbox.setChecked(true);
-
-			linearCheckbox.setChecked(true);
-
-			loopCheckbox.setChecked(true);
-
-			loadScaleSlider.setValue(1);
-			loadScaleSlider.setSnapToValues(new float[] {0.5f, 1, 1.5f, 2, 2.5f}, 0.09f);
-
-			zoomSlider.setValue(1);
-			zoomSlider.setSnapToValues(new float[] {1, 2}, 0.30f);
-
-			xScaleSlider.setValue(1);
-			xScaleSlider.setSnapToValues(new float[] {-1.5f, -1, -0.5f, 0.5f, 1, 1.5f}, 0.12f);
-
-			yScaleSlider.setValue(1);
-			yScaleSlider.setSnapToValues(new float[] {-1.5f, -1, -0.5f, 0.5f, 1, 1.5f}, 0.12f);
-
-			skinList.getSelection().setRequired(false);
-			skinList.getSelection().setToggle(true);
-
-			animationList.getSelection().setRequired(false);
-			animationList.getSelection().setToggle(true);
-
-			mixSlider.setValue(0.3f);
-			mixSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.12f);
-
-			speedSlider.setValue(1);
-			speedSlider.setSnapToValues(new float[] {0.5f, 0.75f, 1, 1.25f, 1.5f, 2, 2.5f}, 0.09f);
-
-			alphaSlider.setValue(1);
-			alphaSlider.setDisabled(true);
-
-			addCheckbox.setDisabled(true);
-			holdPrevCheckbox.setDisabled(true);
-
-			window.setMovable(false);
-			window.setResizable(false);
-			window.setKeepWithinStage(false);
-			window.setX(-3);
-			window.setY(-2);
-
-			window.getTitleLabel().setColor(new Color(0xc1ffffff));
-			window.getTitleTable().add(openButton).space(3);
-			window.getTitleTable().add(minimizeButton).width(20);
-
-			skinScroll.setFadeScrollBars(false);
-
-			animationScroll.setFadeScrollBars(false);
-		}
-
-		void layout () {
-			float resetWidth = loadScaleResetButton.getPrefWidth();
-
-			root.defaults().space(6);
-			root.columnDefaults(0).top().right().padTop(3);
-			root.columnDefaults(1).left();
-			root.add("Load scale:");
-			{
-				Table table = table();
-				table.add(loadScaleLabel).width(29);
-				table.add(loadScaleSlider).growX();
-				table.add(loadScaleResetButton).width(resetWidth);
-				root.add(table).fill().row();
-			}
-			root.add("Zoom:");
-			{
-				Table table = table();
-				table.add(zoomLabel).width(29);
-				table.add(zoomSlider).growX();
-				table.add(zoomResetButton).width(resetWidth);
-				root.add(table).fill().row();
-			}
-			root.add("Scale X:");
-			{
-				Table table = table();
-				table.add(xScaleLabel).width(29);
-				table.add(xScaleSlider).growX();
-				table.add(xScaleResetButton).width(resetWidth);
-				root.add(table).fill().row();
-			}
-			root.add("Scale Y:");
-			{
-				Table table = table();
-				table.add(yScaleLabel).width(29);
-				table.add(yScaleSlider).growX();
-				table.add(yScaleResetButton).width(resetWidth);
-				root.add(table).fill().row();
-			}
-			root.add("Debug:");
-			root.add(table(debugBonesCheckbox, debugRegionsCheckbox, debugBoundingBoxesCheckbox)).row();
-			root.add();
-			root.add(table(debugPathsCheckbox, debugPointsCheckbox, debugClippingCheckbox)).row();
-			root.add();
-			root.add(table(debugMeshHullCheckbox, debugMeshTrianglesCheckbox)).row();
-			root.add("Atlas alpha:");
-			{
-				Table table = table();
-				table.add(premultipliedCheckbox);
-				table.add("Filtering:").growX().getActor().setAlignment(Align.right);
-				table.add(linearCheckbox);
-				root.add(table).fill().row();
-			}
-
-			root.add(new Image(skin.newDrawable("white", new Color(0x4e4e4eff)))).height(1).fillX().colspan(2).pad(-3, 0, 1, 0)
-				.row();
-
-			root.add("Setup pose:");
-			root.add(table(bonesSetupPoseButton, slotsSetupPoseButton, setupPoseButton)).row();
-			root.add("Skin:");
-			root.add(skinScroll).grow().minHeight(64).row();
-
-			root.add(new Image(skin.newDrawable("white", new Color(0x4e4e4eff)))).height(1).fillX().colspan(2).pad(1, 0, 1, 0).row();
-
-			root.add("Track:");
-			{
-				Table table = table();
-				for (TextButton button : trackButtons.getButtons())
-					table.add(button);
-				table.add(loopCheckbox);
-				root.add(table).row();
-			}
-			root.add();
-			{
-				Table table = table();
-				table.add(reverseCheckbox);
-				table.add(holdPrevCheckbox);
-				table.add(addCheckbox);
-				root.add(table).row();
-			}
-			root.add("Entry alpha:");
-			{
-				Table table = table();
-				table.add(alphaLabel).width(29);
-				table.add(alphaSlider).growX();
-				root.add(table).fill().row();
-			}
-			root.add("Animation:");
-			root.add(animationScroll).grow().minHeight(64).row();
-
-			root.add(new Image(skin.newDrawable("white", new Color(0x4e4e4eff)))).height(1).fillX().colspan(2).pad(1, 0, 1, 0).row();
-
-			root.add("Speed:");
-			{
-				Table table = table();
-				table.add(speedLabel).width(29);
-				table.add(speedSlider).growX();
-				table.add(speedResetButton);
-				root.add(table).fill().row();
-			}
-			root.add("Default mix:");
-			{
-				Table table = table();
-				table.add(mixLabel).width(29);
-				table.add(mixSlider).growX();
-				root.add(table).fill().row();
-			}
-
-			window.add(root).grow();
-			window.pack();
-			stage.addActor(window);
-
-			stage.addActor(statusLabel);
-
-			{
-				Table table = new Table();
-				table.setFillParent(true);
-				table.setTouchable(Touchable.disabled);
-				stage.addActor(table);
-				table.pad(10, 10, 22, 10).bottom().right();
-				table.add(toasts);
-			}
-
-			{
-				Table table = new Table();
-				table.setFillParent(true);
-				table.setTouchable(Touchable.disabled);
-				stage.addActor(table);
-				table.pad(10).top().right();
-				table.defaults().right();
-				table.add(new Label(version, skin, "default", Color.LIGHT_GRAY));
-			}
-		}
-
-		void events () {
-			window.addListener(new InputListener() {
-				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
-					event.cancel();
-					return true;
-				}
-			});
-
-			openButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					FileDialog fileDialog = new FileDialog((Frame)null, "Choose skeleton file");
-					fileDialog.setMode(FileDialog.LOAD);
-					fileDialog.setVisible(true);
-					String name = fileDialog.getFile();
-					String dir = fileDialog.getDirectory();
-					if (name == null || dir == null) return;
-					loadSkeleton(new FileHandle(new File(dir, name).getAbsolutePath()));
-					ui.toast("Loaded.");
-				}
-			});
-
-			setupPoseButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (skeleton != null) skeleton.setToSetupPose();
-				}
-			});
-			bonesSetupPoseButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (skeleton != null) skeleton.setBonesToSetupPose();
-				}
-			});
-			slotsSetupPoseButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					skeleton.getRootBone().getChildren();
-					if (skeleton != null) skeleton.setSlotsToSetupPose();
-				}
-			});
-
-			minimizeButton.addListener(new ClickListener() {
-				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
-					event.cancel();
-					return super.touchDown(event, x, y, pointer, button);
-				}
-
-				public void clicked (InputEvent event, float x, float y) {
-					if (minimizeButton.isChecked()) {
-						window.getCells().get(0).setActor(null);
-						window.setHeight(37);
-						minimizeButton.setText("+");
-					} else {
-						window.getCells().get(0).setActor(root);
-						window.setHeight(Gdx.graphics.getHeight() / uiScale + 8);
-						minimizeButton.setText("-");
-					}
-				}
-			});
-
-			loadScaleSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					loadScaleLabel.setText(Integer.toString((int)(loadScaleSlider.getValue() * 100)) + "%");
-					if (!loadScaleSlider.isDragging()) {
-						loadSkeleton(skeletonFile);
-						ui.toast("Reloaded.");
-					}
-					loadScaleResetButton.setText(loadScaleSlider.getValue() == 1 ? "Reload" : "Reset");
-				}
-			});
-			loadScaleResetButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					resetCameraPosition();
-					if (loadScaleSlider.getValue() == 1) {
-						loadSkeleton(skeletonFile);
-						ui.toast("Reloaded.");
-					} else
-						loadScaleSlider.setValue(1);
-					loadScaleResetButton.setText("Reload");
-				}
-			});
-
-			zoomSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					zoomLabel.setText(Integer.toString((int)(zoomSlider.getValue() * 100)) + "%");
-					float newZoom = 1 / zoomSlider.getValue();
-					camera.position.x -= window.getWidth() / 2 * (newZoom - camera.zoom);
-					camera.zoom = newZoom;
-				}
-			});
-			zoomResetButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					resetCameraPosition();
-					float x = camera.position.x;
-					zoomSlider.setValue(1);
-					camera.position.x = x;
-				}
-			});
-
-			xScaleSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (xScaleSlider.getValue() == 0) xScaleSlider.setValue(0.01f);
-					xScaleLabel.setText(Integer.toString((int)(xScaleSlider.getValue() * 100)) + "%");
-				}
-			});
-			xScaleResetButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					xScaleSlider.setValue(1);
-				}
-			});
-
-			yScaleSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (yScaleSlider.getValue() == 0) yScaleSlider.setValue(0.01f);
-					yScaleLabel.setText(Integer.toString((int)(yScaleSlider.getValue() * 100)) + "%");
-				}
-			});
-			yScaleResetButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					yScaleSlider.setValue(1);
-				}
-			});
-
-			speedSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					speedLabel.setText(Float.toString((int)(speedSlider.getValue() * 100) / 100f) + "x");
-				}
-			});
-			speedResetButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					speedSlider.setValue(1);
-				}
-			});
-
-			alphaSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					alphaLabel.setText(Integer.toString((int)(alphaSlider.getValue() * 100)) + "%");
-					int track = trackButtons.getCheckedIndex();
-					if (track > 0) {
-						TrackEntry current = state.getCurrent(track);
-						if (current != null) {
-							current.setAlpha(alphaSlider.getValue());
-							current.resetRotationDirections();
-						}
-					}
-				}
-			});
-
-			mixSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					mixLabel.setText(Float.toString((int)(mixSlider.getValue() * 100) / 100f) + "s");
-					if (state != null) state.getData().setDefaultMix(mixSlider.getValue());
-				}
-			});
-
-			InputListener scrollFocusListener = new InputListener() {
-				public void enter (InputEvent event, float x, float y, int pointer, @Null Actor fromActor) {
-					if (pointer == -1) stage.setScrollFocus(event.getListenerActor());
-				}
-
-				public void exit (InputEvent event, float x, float y, int pointer, @Null Actor toActor) {
-					if (pointer == -1 && stage.getScrollFocus() == event.getListenerActor()) stage.setScrollFocus(null);
-				}
-			};
-
-			animationList.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (state != null) {
-						String name = animationList.getSelected();
-						if (name == null)
-							state.setEmptyAnimation(trackButtons.getCheckedIndex(), mixSlider.getValue());
-						else
-							setAnimation();
-					}
-				}
-			});
-			animationScroll.addListener(scrollFocusListener);
-
-			ChangeListener setAnimation = new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					setAnimation();
-				}
-			};
-			loopCheckbox.addListener(setAnimation);
-			reverseCheckbox.addListener(setAnimation);
-			holdPrevCheckbox.addListener(setAnimation);
-			addCheckbox.addListener(setAnimation);
-
-			linearCheckbox.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (atlas == null) return;
-					TextureFilter filter = linearCheckbox.isChecked() ? TextureFilter.Linear : TextureFilter.Nearest;
-					for (Texture texture : atlas.getTextures())
-						texture.setFilter(filter, filter);
-				}
-			});
-
-			skinList.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (skeleton != null) {
-						String skinName = skinList.getSelected();
-						if (skinName == null)
-							skeleton.setSkin((Skin)null);
-						else
-							skeleton.setSkin(skinName);
-						skeleton.setSlotsToSetupPose();
-					}
-				}
-			});
-			skinScroll.addListener(scrollFocusListener);
-
-			ChangeListener trackButtonListener = new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					int track = trackButtons.getCheckedIndex();
-					if (track == -1) return;
-					TrackEntry current = state.getCurrent(track);
-					animationList.getSelection().setProgrammaticChangeEvents(false);
-					animationList.setSelected(current == null ? null : current.animation.name);
-					animationList.getSelection().setProgrammaticChangeEvents(true);
-
-					alphaSlider.setDisabled(track == 0);
-					alphaSlider.setValue(current == null ? 1 : current.alpha);
-
-					addCheckbox.setDisabled(track == 0);
-					holdPrevCheckbox.setDisabled(track == 0);
-
-					if (current != null) {
-						loopCheckbox.setChecked(current.getLoop());
-						addCheckbox.setChecked(current.getMixBlend() == MixBlend.add);
-						reverseCheckbox.setChecked(current.getReverse());
-						holdPrevCheckbox.setChecked(current.getHoldPrevious());
-					}
-				}
-			};
-			for (TextButton button : trackButtons.getButtons())
-				button.addListener(trackButtonListener);
-
-			Gdx.input.setInputProcessor(new InputMultiplexer(stage, new InputAdapter() {
-				float offsetX;
-				float offsetY;
-
-				public boolean touchDown (int screenX, int screenY, int pointer, int button) {
-					offsetX = screenX;
-					offsetY = Gdx.graphics.getHeight() - 1 - screenY;
-					return false;
-				}
-
-				public boolean touchDragged (int screenX, int screenY, int pointer) {
-					float deltaX = screenX - offsetX;
-					float deltaY = Gdx.graphics.getHeight() - 1 - screenY - offsetY;
-
-					camera.position.x -= deltaX * camera.zoom;
-					camera.position.y -= deltaY * camera.zoom;
-
-					offsetX = screenX;
-					offsetY = Gdx.graphics.getHeight() - 1 - screenY;
-					return false;
-				}
-
-				public boolean touchUp (int screenX, int screenY, int pointer, int button) {
-					savePrefs();
-					return false;
-				}
-
-				public boolean scrolled (float amountX, float amountY) {
-					float zoom = zoomSlider.getValue(), zoomMin = zoomSlider.getMinValue(), zoomMax = zoomSlider.getMaxValue();
-					float speedAlpha = Math.min(1.2f, (zoom - zoomMin) / (zoomMax - zoomMin) * 3.5f);
-					zoom -= linear.apply(0.02f, 0.2f, speedAlpha) * Math.signum(amountY);
-					zoomSlider.setValue(MathUtils.clamp(zoom, zoomMin, zoomMax));
-					return false;
-				}
-			}));
-
-			ChangeListener savePrefsListener = new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (actor instanceof Slider && ((Slider)actor).isDragging()) return;
-					savePrefs();
-				}
-			};
-			debugBonesCheckbox.addListener(savePrefsListener);
-			debugRegionsCheckbox.addListener(savePrefsListener);
-			debugMeshHullCheckbox.addListener(savePrefsListener);
-			debugMeshTrianglesCheckbox.addListener(savePrefsListener);
-			debugPathsCheckbox.addListener(savePrefsListener);
-			debugPointsCheckbox.addListener(savePrefsListener);
-			debugClippingCheckbox.addListener(savePrefsListener);
-			premultipliedCheckbox.addListener(savePrefsListener);
-			loopCheckbox.addListener(savePrefsListener);
-			addCheckbox.addListener(savePrefsListener);
-			holdPrevCheckbox.addListener(savePrefsListener);
-			reverseCheckbox.addListener(savePrefsListener);
-			speedSlider.addListener(savePrefsListener);
-			speedResetButton.addListener(savePrefsListener);
-			mixSlider.addListener(savePrefsListener);
-			loadScaleSlider.addListener(savePrefsListener);
-			loadScaleResetButton.addListener(savePrefsListener);
-			zoomSlider.addListener(savePrefsListener);
-			zoomResetButton.addListener(savePrefsListener);
-			animationList.addListener(savePrefsListener);
-			skinList.addListener(savePrefsListener);
-		}
-
-		Table table (Actor... actors) {
-			Table table = new Table(skin);
-			table.defaults().space(6);
-			table.add(actors);
-			return table;
-		}
-
-		void render () {
-			if (state != null && state.getCurrent(trackButtons.getCheckedIndex()) == null) {
-				animationList.getSelection().setProgrammaticChangeEvents(false);
-				animationList.setSelected(null);
-				animationList.getSelection().setProgrammaticChangeEvents(true);
-			}
-
-			statusLabel.pack();
-			if (minimizeButton.isChecked())
-				statusLabel.setPosition(10, 25, Align.bottom | Align.left);
-			else
-				statusLabel.setPosition(window.getWidth() + 6, 5, Align.bottom | Align.left);
-
-			stage.act();
-			stage.draw();
-		}
-
-		void toast (String text) {
-			Table table = new Table();
-			table.add(new Label(text, skin));
-			table.getColor().a = 0;
-			table.pack();
-			table.setPosition(-table.getWidth(), -3 - table.getHeight());
-			table.addAction(sequence( //
-				parallel(moveBy(0, table.getHeight(), 0.3f), fadeIn(0.3f)), //
-				delay(5f), //
-				parallel(moveBy(0, table.getHeight(), 0.3f), fadeOut(0.3f)), //
-				removeActor() //
-			));
-			for (Actor actor : toasts.getChildren())
-				actor.addAction(moveBy(0, table.getHeight(), 0.3f));
-			toasts.addActor(table);
-			toasts.getParent().toFront();
-		}
-
-		void savePrefs () {
-			if (!prefsLoaded) return;
-			prefs.putBoolean("debugBones", debugBonesCheckbox.isChecked());
-			prefs.putBoolean("debugRegions", debugRegionsCheckbox.isChecked());
-			prefs.putBoolean("debugMeshHull", debugMeshHullCheckbox.isChecked());
-			prefs.putBoolean("debugMeshTriangles", debugMeshTrianglesCheckbox.isChecked());
-			prefs.putBoolean("debugPaths", debugPathsCheckbox.isChecked());
-			prefs.putBoolean("debugPoints", debugPointsCheckbox.isChecked());
-			prefs.putBoolean("debugClipping", debugClippingCheckbox.isChecked());
-			prefs.putBoolean("premultiplied", premultipliedCheckbox.isChecked());
-			prefs.putBoolean("loop", loopCheckbox.isChecked());
-			prefs.putBoolean("add", addCheckbox.isChecked());
-			prefs.putBoolean("holdPrev", holdPrevCheckbox.isChecked());
-			prefs.putBoolean("reverse", reverseCheckbox.isChecked());
-			prefs.putFloat("speed", speedSlider.getValue());
-			prefs.putFloat("mix", mixSlider.getValue());
-			prefs.putFloat("scale", loadScaleSlider.getValue());
-			prefs.putFloat("zoom", zoomSlider.getValue());
-			prefs.putFloat("x", camera.position.x);
-			prefs.putFloat("y", camera.position.y);
-			if (state != null) {
-				TrackEntry current = state.getCurrent(0);
-				if (current != null) {
-					String name = current.animation.name;
-					if (name.equals("<empty>")) name = current.next == null ? "" : current.next.animation.name;
-					prefs.putString("animationName", name);
-				}
-			}
-			if (skinList.getSelected() != null) prefs.putString("skinName", skinList.getSelected());
-			prefs.flush();
-		}
-
-		void loadPrefs () {
-			try {
-				debugBonesCheckbox.setChecked(prefs.getBoolean("debugBones", true));
-				debugRegionsCheckbox.setChecked(prefs.getBoolean("debugRegions", false));
-				debugMeshHullCheckbox.setChecked(prefs.getBoolean("debugMeshHull", false));
-				debugMeshTrianglesCheckbox.setChecked(prefs.getBoolean("debugMeshTriangles", false));
-				debugPathsCheckbox.setChecked(prefs.getBoolean("debugPaths", true));
-				debugPointsCheckbox.setChecked(prefs.getBoolean("debugPoints", true));
-				debugClippingCheckbox.setChecked(prefs.getBoolean("debugClipping", true));
-				premultipliedCheckbox.setChecked(prefs.getBoolean("premultiplied", true));
-				loopCheckbox.setChecked(prefs.getBoolean("loop", true));
-				addCheckbox.setChecked(prefs.getBoolean("add", false));
-				holdPrevCheckbox.setChecked(prefs.getBoolean("holdPrev", false));
-				reverseCheckbox.setChecked(prefs.getBoolean("reverse", false));
-				speedSlider.setValue(prefs.getFloat("speed", 0.3f));
-				mixSlider.setValue(prefs.getFloat("mix", 0.3f));
-
-				zoomSlider.setValue(prefs.getFloat("zoom", 1));
-				camera.zoom = 1 / prefs.getFloat("zoom", 1);
-				camera.position.x = prefs.getFloat("x", 0);
-				camera.position.y = prefs.getFloat("y", 0);
-
-				loadScaleSlider.setValue(prefs.getFloat("scale", 1));
-				animationList.setSelected(prefs.getString("animationName", null));
-				skinList.setSelected(prefs.getString("skinName", null));
-			} catch (Throwable ex) {
-				System.out.println("Unable to read preferences:");
-				ex.printStackTrace();
-			}
-		}
-	}
-
 	static public void main (String[] args) throws Exception {
 		try { // Try to turn off illegal access log messages.
 			Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");

+ 723 - 0
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewerUI.java

@@ -0,0 +1,723 @@
+
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.Interpolation.*;
+import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
+
+import java.io.File;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.InputAdapter;
+import com.badlogic.gdx.InputMultiplexer;
+import com.badlogic.gdx.Preferences;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.Texture.TextureFilter;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.scenes.scene2d.Actor;
+import com.badlogic.gdx.scenes.scene2d.InputEvent;
+import com.badlogic.gdx.scenes.scene2d.InputListener;
+import com.badlogic.gdx.scenes.scene2d.Stage;
+import com.badlogic.gdx.scenes.scene2d.Touchable;
+import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup;
+import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
+import com.badlogic.gdx.scenes.scene2d.ui.Image;
+import com.badlogic.gdx.scenes.scene2d.ui.Label;
+import com.badlogic.gdx.scenes.scene2d.ui.List;
+import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
+import com.badlogic.gdx.scenes.scene2d.ui.Slider;
+import com.badlogic.gdx.scenes.scene2d.ui.Table;
+import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
+import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup;
+import com.badlogic.gdx.scenes.scene2d.ui.Window;
+import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
+import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
+import com.badlogic.gdx.utils.Align;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.viewport.ScreenViewport;
+
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+
+import java.awt.FileDialog;
+import java.awt.Frame;
+
+class SkeletonViewerUI {
+	final SkeletonViewer viewer;
+	final OrthographicCamera camera;
+
+	boolean prefsLoaded;
+
+	Stage stage = new Stage(new ScreenViewport());
+	com.badlogic.gdx.scenes.scene2d.ui.Skin skin = new com.badlogic.gdx.scenes.scene2d.ui.Skin(
+		Gdx.files.internal("skin/skin.json"));
+
+	Window window = new Window("Skeleton", skin);
+	Table root = new Table(skin);
+	TextButton openButton = new TextButton("Open", skin);
+	TextButton minimizeButton = new TextButton("-", skin);
+
+	Slider loadScaleSlider = new Slider(0.1f, 3, 0.01f, false, skin);
+	Label loadScaleLabel = new Label("100%", skin);
+	TextButton loadScaleResetButton = new TextButton("Reload", skin);
+
+	Slider zoomSlider = new Slider(0.01f, 10, 0.01f, false, skin);
+	Label zoomLabel = new Label("100%", skin);
+	TextButton zoomResetButton = new TextButton("Reset", skin);
+
+	Slider xScaleSlider = new Slider(-2, 2, 0.01f, false, skin);
+	Label xScaleLabel = new Label("100%", skin);
+	TextButton xScaleResetButton = new TextButton("Reset", skin);
+
+	Slider yScaleSlider = new Slider(-2, 2, 0.01f, false, skin);
+	Label yScaleLabel = new Label("100%", skin);
+	TextButton yScaleResetButton = new TextButton("Reset", skin);
+
+	CheckBox debugBonesCheckbox = new CheckBox("Bones", skin);
+	CheckBox debugRegionsCheckbox = new CheckBox("Regions", skin);
+	CheckBox debugBoundingBoxesCheckbox = new CheckBox("Bounds", skin);
+	CheckBox debugMeshHullCheckbox = new CheckBox("Mesh hull", skin);
+	CheckBox debugMeshTrianglesCheckbox = new CheckBox("Triangles", skin);
+	CheckBox debugPathsCheckbox = new CheckBox("Paths", skin);
+	CheckBox debugPointsCheckbox = new CheckBox("Points", skin);
+	CheckBox debugClippingCheckbox = new CheckBox("Clipping", skin);
+
+	CheckBox pmaCheckbox = new CheckBox("Premultiplied", skin);
+
+	CheckBox linearCheckbox = new CheckBox("Linear", skin);
+
+	TextButton bonesSetupPoseButton = new TextButton("Bones", skin);
+	TextButton slotsSetupPoseButton = new TextButton("Slots", skin);
+	TextButton setupPoseButton = new TextButton("Both", skin);
+
+	List<String> skinList = new List(skin);
+	ScrollPane skinScroll = new ScrollPane(skinList, skin, "bg");
+
+	ButtonGroup<TextButton> trackButtons = new ButtonGroup();
+	CheckBox loopCheckbox = new CheckBox("Loop", skin);
+	CheckBox addCheckbox = new CheckBox("Add", skin);
+
+	Slider alphaSlider = new Slider(0, 1, 0.01f, false, skin);
+	Label alphaLabel = new Label("100%", skin);
+
+	List<String> animationList = new List(skin);
+	ScrollPane animationScroll = new ScrollPane(animationList, skin, "bg");
+
+	Slider speedSlider = new Slider(0, 3, 0.01f, false, skin);
+	Label speedLabel = new Label("1.0x", skin);
+	TextButton speedResetButton = new TextButton("Reset", skin);
+
+	CheckBox reverseCheckbox = new CheckBox("Reverse", skin);
+	CheckBox holdPrevCheckbox = new CheckBox("Hold previous", skin);
+
+	Slider mixSlider = new Slider(0, 4, 0.01f, false, skin);
+	Label mixLabel = new Label("0.3s", skin);
+
+	Label statusLabel = new Label("", skin);
+	WidgetGroup toasts = new WidgetGroup();
+
+	SkeletonViewerUI (SkeletonViewer viewer) {
+		this.viewer = viewer;
+		camera = viewer.camera;
+		initialize();
+		layout();
+		events();
+	}
+
+	void initialize () {
+		skin.getFont("default").getData().markupEnabled = true;
+
+		for (int i = 0; i < 6; i++)
+			trackButtons.add(new TextButton(i + "", skin, "toggle"));
+
+		pmaCheckbox.setChecked(true);
+
+		linearCheckbox.setChecked(true);
+
+		loopCheckbox.setChecked(true);
+
+		loadScaleSlider.setValue(1);
+		loadScaleSlider.setSnapToValues(new float[] {0.5f, 1, 1.5f, 2, 2.5f}, 0.09f);
+
+		zoomSlider.setValue(1);
+		zoomSlider.setSnapToValues(new float[] {1, 2}, 0.30f);
+
+		xScaleSlider.setValue(1);
+		xScaleSlider.setSnapToValues(new float[] {-1.5f, -1, -0.5f, 0.5f, 1, 1.5f}, 0.12f);
+
+		yScaleSlider.setValue(1);
+		yScaleSlider.setSnapToValues(new float[] {-1.5f, -1, -0.5f, 0.5f, 1, 1.5f}, 0.12f);
+
+		skinList.getSelection().setRequired(false);
+		skinList.getSelection().setToggle(true);
+
+		animationList.getSelection().setRequired(false);
+		animationList.getSelection().setToggle(true);
+
+		mixSlider.setValue(0.3f);
+		mixSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.12f);
+
+		speedSlider.setValue(1);
+		speedSlider.setSnapToValues(new float[] {0.5f, 0.75f, 1, 1.25f, 1.5f, 2, 2.5f}, 0.09f);
+
+		alphaSlider.setValue(1);
+		alphaSlider.setDisabled(true);
+
+		addCheckbox.setDisabled(true);
+		holdPrevCheckbox.setDisabled(true);
+
+		window.setMovable(false);
+		window.setResizable(false);
+		window.setKeepWithinStage(false);
+		window.setX(-3);
+		window.setY(-2);
+
+		window.getTitleLabel().setColor(new Color(0xc1ffffff));
+		window.getTitleTable().add(openButton).space(3);
+		window.getTitleTable().add(minimizeButton).width(20);
+
+		skinScroll.setFadeScrollBars(false);
+
+		animationScroll.setFadeScrollBars(false);
+	}
+
+	void layout () {
+		float resetWidth = loadScaleResetButton.getPrefWidth();
+
+		root.defaults().space(6);
+		root.columnDefaults(0).top().right().padTop(3);
+		root.columnDefaults(1).left();
+		root.add("Load scale:");
+		{
+			Table table = table();
+			table.add(loadScaleLabel).width(29);
+			table.add(loadScaleSlider).growX();
+			table.add(loadScaleResetButton).width(resetWidth);
+			root.add(table).fill().row();
+		}
+		root.add("Zoom:");
+		{
+			Table table = table();
+			table.add(zoomLabel).width(29);
+			table.add(zoomSlider).growX();
+			table.add(zoomResetButton).width(resetWidth);
+			root.add(table).fill().row();
+		}
+		root.add("Scale X:");
+		{
+			Table table = table();
+			table.add(xScaleLabel).width(29);
+			table.add(xScaleSlider).growX();
+			table.add(xScaleResetButton).width(resetWidth);
+			root.add(table).fill().row();
+		}
+		root.add("Scale Y:");
+		{
+			Table table = table();
+			table.add(yScaleLabel).width(29);
+			table.add(yScaleSlider).growX();
+			table.add(yScaleResetButton).width(resetWidth);
+			root.add(table).fill().row();
+		}
+		root.add("Debug:");
+		root.add(table(debugBonesCheckbox, debugRegionsCheckbox, debugBoundingBoxesCheckbox)).row();
+		root.add();
+		root.add(table(debugPathsCheckbox, debugPointsCheckbox, debugClippingCheckbox)).row();
+		root.add();
+		root.add(table(debugMeshHullCheckbox, debugMeshTrianglesCheckbox)).row();
+		root.add("Atlas alpha:");
+		{
+			Table table = table();
+			table.add(pmaCheckbox);
+			table.add("Filtering:").growX().getActor().setAlignment(Align.right);
+			table.add(linearCheckbox);
+			root.add(table).fill().row();
+		}
+
+		root.add(new Image(skin.newDrawable("white", new Color(0x4e4e4eff)))).height(1).fillX().colspan(2).pad(-3, 0, 1, 0).row();
+
+		root.add("Setup pose:");
+		root.add(table(bonesSetupPoseButton, slotsSetupPoseButton, setupPoseButton)).row();
+		root.add("Skin:");
+		root.add(skinScroll).grow().minHeight(64).row();
+
+		root.add(new Image(skin.newDrawable("white", new Color(0x4e4e4eff)))).height(1).fillX().colspan(2).pad(1, 0, 1, 0).row();
+
+		root.add("Track:");
+		{
+			Table table = table();
+			for (TextButton button : trackButtons.getButtons())
+				table.add(button);
+			table.add(loopCheckbox);
+			root.add(table).row();
+		}
+		root.add();
+		{
+			Table table = table();
+			table.add(reverseCheckbox);
+			table.add(holdPrevCheckbox);
+			table.add(addCheckbox);
+			root.add(table).row();
+		}
+		root.add("Entry alpha:");
+		{
+			Table table = table();
+			table.add(alphaLabel).width(29);
+			table.add(alphaSlider).growX();
+			root.add(table).fill().row();
+		}
+		root.add("Animation:");
+		root.add(animationScroll).grow().minHeight(64).row();
+
+		root.add(new Image(skin.newDrawable("white", new Color(0x4e4e4eff)))).height(1).fillX().colspan(2).pad(1, 0, 1, 0).row();
+
+		root.add("Speed:");
+		{
+			Table table = table();
+			table.add(speedLabel).width(29);
+			table.add(speedSlider).growX();
+			table.add(speedResetButton);
+			root.add(table).fill().row();
+		}
+		root.add("Default mix:");
+		{
+			Table table = table();
+			table.add(mixLabel).width(29);
+			table.add(mixSlider).growX();
+			root.add(table).fill().row();
+		}
+
+		window.add(root).grow();
+		window.pack();
+		stage.addActor(window);
+
+		stage.addActor(statusLabel);
+
+		{
+			Table table = new Table();
+			table.setFillParent(true);
+			table.setTouchable(Touchable.disabled);
+			stage.addActor(table);
+			table.pad(10, 10, 22, 10).bottom().right();
+			table.add(toasts);
+		}
+
+		{
+			Table table = new Table();
+			table.setFillParent(true);
+			table.setTouchable(Touchable.disabled);
+			stage.addActor(table);
+			table.pad(10).top().right();
+			table.defaults().right();
+			table.add(new Label(SkeletonViewer.version, skin, "default", Color.LIGHT_GRAY));
+		}
+	}
+
+	void events () {
+		window.addListener(new InputListener() {
+			public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
+				event.cancel();
+				return true;
+			}
+		});
+
+		openButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				FileDialog fileDialog = new FileDialog((Frame)null, "Choose skeleton file");
+				fileDialog.setMode(FileDialog.LOAD);
+				fileDialog.setVisible(true);
+				String name = fileDialog.getFile();
+				String dir = fileDialog.getDirectory();
+				if (name == null || dir == null) return;
+				viewer.loadSkeleton(new FileHandle(new File(dir, name).getAbsolutePath()));
+				toast("Loaded.");
+			}
+		});
+
+		setupPoseButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (viewer.skeleton != null) viewer.skeleton.setToSetupPose();
+			}
+		});
+		bonesSetupPoseButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (viewer.skeleton != null) viewer.skeleton.setBonesToSetupPose();
+			}
+		});
+		slotsSetupPoseButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				viewer.skeleton.getRootBone().getChildren();
+				if (viewer.skeleton != null) viewer.skeleton.setSlotsToSetupPose();
+			}
+		});
+
+		minimizeButton.addListener(new ClickListener() {
+			public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
+				event.cancel();
+				return super.touchDown(event, x, y, pointer, button);
+			}
+
+			public void clicked (InputEvent event, float x, float y) {
+				if (minimizeButton.isChecked()) {
+					window.getCells().get(0).setActor(null);
+					window.setHeight(37);
+					minimizeButton.setText("+");
+				} else {
+					window.getCells().get(0).setActor(root);
+					window.setHeight(Gdx.graphics.getHeight() / SkeletonViewer.uiScale + 8);
+					minimizeButton.setText("-");
+				}
+			}
+		});
+
+		loadScaleSlider.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				loadScaleLabel.setText(Integer.toString((int)(loadScaleSlider.getValue() * 100)) + "%");
+				if (!loadScaleSlider.isDragging()) {
+					viewer.loadSkeleton(viewer.skeletonFile);
+					toast("Reloaded.");
+				}
+				loadScaleResetButton.setText(loadScaleSlider.getValue() == 1 ? "Reload" : "Reset");
+			}
+		});
+		loadScaleResetButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				viewer.resetCameraPosition();
+				if (loadScaleSlider.getValue() == 1) {
+					viewer.loadSkeleton(viewer.skeletonFile);
+					toast("Reloaded.");
+				} else
+					loadScaleSlider.setValue(1);
+				loadScaleResetButton.setText("Reload");
+			}
+		});
+
+		zoomSlider.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				zoomLabel.setText(Integer.toString((int)(zoomSlider.getValue() * 100)) + "%");
+				float newZoom = 1 / zoomSlider.getValue();
+				camera.position.x -= window.getWidth() / 2 * (newZoom - camera.zoom);
+				camera.zoom = newZoom;
+			}
+		});
+		zoomResetButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				viewer.resetCameraPosition();
+				float x = camera.position.x;
+				zoomSlider.setValue(1);
+				camera.position.x = x;
+			}
+		});
+
+		xScaleSlider.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (xScaleSlider.getValue() == 0) xScaleSlider.setValue(0.01f);
+				xScaleLabel.setText(Integer.toString((int)(xScaleSlider.getValue() * 100)) + "%");
+			}
+		});
+		xScaleResetButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				xScaleSlider.setValue(1);
+			}
+		});
+
+		yScaleSlider.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (yScaleSlider.getValue() == 0) yScaleSlider.setValue(0.01f);
+				yScaleLabel.setText(Integer.toString((int)(yScaleSlider.getValue() * 100)) + "%");
+			}
+		});
+		yScaleResetButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				yScaleSlider.setValue(1);
+			}
+		});
+
+		speedSlider.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				speedLabel.setText(Float.toString((int)(speedSlider.getValue() * 100) / 100f) + "x");
+			}
+		});
+		speedResetButton.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				speedSlider.setValue(1);
+			}
+		});
+
+		alphaSlider.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				alphaLabel.setText(Integer.toString((int)(alphaSlider.getValue() * 100)) + "%");
+				int track = trackButtons.getCheckedIndex();
+				if (track > 0) {
+					TrackEntry current = viewer.state.getCurrent(track);
+					if (current != null) {
+						current.setAlpha(alphaSlider.getValue());
+						current.resetRotationDirections();
+					}
+				}
+			}
+		});
+
+		mixSlider.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				mixLabel.setText(Float.toString((int)(mixSlider.getValue() * 100) / 100f) + "s");
+				if (viewer.state != null) viewer.state.getData().setDefaultMix(mixSlider.getValue());
+			}
+		});
+
+		InputListener scrollFocusListener = new InputListener() {
+			public void enter (InputEvent event, float x, float y, int pointer, @Null Actor fromActor) {
+				if (pointer == -1) stage.setScrollFocus(event.getListenerActor());
+			}
+
+			public void exit (InputEvent event, float x, float y, int pointer, @Null Actor toActor) {
+				if (pointer == -1 && stage.getScrollFocus() == event.getListenerActor()) stage.setScrollFocus(null);
+			}
+		};
+
+		animationList.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (viewer.state != null) {
+					String name = animationList.getSelected();
+					if (name == null)
+						viewer.state.setEmptyAnimation(trackButtons.getCheckedIndex(), mixSlider.getValue());
+					else
+						viewer.setAnimation();
+				}
+			}
+		});
+		animationScroll.addListener(scrollFocusListener);
+
+		ChangeListener setAnimation = new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				viewer.setAnimation();
+			}
+		};
+		loopCheckbox.addListener(setAnimation);
+		reverseCheckbox.addListener(setAnimation);
+		holdPrevCheckbox.addListener(setAnimation);
+		addCheckbox.addListener(setAnimation);
+
+		linearCheckbox.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (viewer.atlas == null) return;
+				TextureFilter filter = linearCheckbox.isChecked() ? TextureFilter.Linear : TextureFilter.Nearest;
+				for (Texture texture : viewer.atlas.getTextures())
+					texture.setFilter(filter, filter);
+			}
+		});
+
+		skinList.addListener(new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (viewer.skeleton != null) {
+					String skinName = skinList.getSelected();
+					if (skinName == null)
+						viewer.skeleton.setSkin((Skin)null);
+					else
+						viewer.skeleton.setSkin(skinName);
+					viewer.skeleton.setSlotsToSetupPose();
+				}
+			}
+		});
+		skinScroll.addListener(scrollFocusListener);
+
+		ChangeListener trackButtonListener = new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				int track = trackButtons.getCheckedIndex();
+				if (track == -1) return;
+				TrackEntry current = viewer.state.getCurrent(track);
+				animationList.getSelection().setProgrammaticChangeEvents(false);
+				animationList.setSelected(current == null ? null : current.animation.name);
+				animationList.getSelection().setProgrammaticChangeEvents(true);
+
+				alphaSlider.setDisabled(track == 0);
+				alphaSlider.setValue(current == null ? 1 : current.alpha);
+
+				addCheckbox.setDisabled(track == 0);
+				holdPrevCheckbox.setDisabled(track == 0);
+
+				if (current != null) {
+					loopCheckbox.setChecked(current.getLoop());
+					addCheckbox.setChecked(current.getMixBlend() == MixBlend.add);
+					reverseCheckbox.setChecked(current.getReverse());
+					holdPrevCheckbox.setChecked(current.getHoldPrevious());
+				}
+			}
+		};
+		for (TextButton button : trackButtons.getButtons())
+			button.addListener(trackButtonListener);
+
+		Gdx.input.setInputProcessor(new InputMultiplexer(stage, new InputAdapter() {
+			float offsetX;
+			float offsetY;
+
+			public boolean touchDown (int screenX, int screenY, int pointer, int button) {
+				offsetX = screenX;
+				offsetY = Gdx.graphics.getHeight() - 1 - screenY;
+				return false;
+			}
+
+			public boolean touchDragged (int screenX, int screenY, int pointer) {
+				float deltaX = screenX - offsetX;
+				float deltaY = Gdx.graphics.getHeight() - 1 - screenY - offsetY;
+
+				camera.position.x -= deltaX * camera.zoom;
+				camera.position.y -= deltaY * camera.zoom;
+
+				offsetX = screenX;
+				offsetY = Gdx.graphics.getHeight() - 1 - screenY;
+				return false;
+			}
+
+			public boolean touchUp (int screenX, int screenY, int pointer, int button) {
+				savePrefs();
+				return false;
+			}
+
+			public boolean scrolled (float amountX, float amountY) {
+				float zoom = zoomSlider.getValue(), zoomMin = zoomSlider.getMinValue(), zoomMax = zoomSlider.getMaxValue();
+				float speedAlpha = Math.min(1.2f, (zoom - zoomMin) / (zoomMax - zoomMin) * 3.5f);
+				zoom -= linear.apply(0.02f, 0.2f, speedAlpha) * Math.signum(amountY);
+				zoomSlider.setValue(MathUtils.clamp(zoom, zoomMin, zoomMax));
+				return false;
+			}
+		}));
+
+		ChangeListener savePrefsListener = new ChangeListener() {
+			public void changed (ChangeEvent event, Actor actor) {
+				if (actor instanceof Slider && ((Slider)actor).isDragging()) return;
+				savePrefs();
+			}
+		};
+		debugBonesCheckbox.addListener(savePrefsListener);
+		debugRegionsCheckbox.addListener(savePrefsListener);
+		debugMeshHullCheckbox.addListener(savePrefsListener);
+		debugMeshTrianglesCheckbox.addListener(savePrefsListener);
+		debugPathsCheckbox.addListener(savePrefsListener);
+		debugPointsCheckbox.addListener(savePrefsListener);
+		debugClippingCheckbox.addListener(savePrefsListener);
+		pmaCheckbox.addListener(savePrefsListener);
+		loopCheckbox.addListener(savePrefsListener);
+		addCheckbox.addListener(savePrefsListener);
+		holdPrevCheckbox.addListener(savePrefsListener);
+		reverseCheckbox.addListener(savePrefsListener);
+		speedSlider.addListener(savePrefsListener);
+		speedResetButton.addListener(savePrefsListener);
+		mixSlider.addListener(savePrefsListener);
+		loadScaleSlider.addListener(savePrefsListener);
+		loadScaleResetButton.addListener(savePrefsListener);
+		zoomSlider.addListener(savePrefsListener);
+		zoomResetButton.addListener(savePrefsListener);
+		animationList.addListener(savePrefsListener);
+		skinList.addListener(savePrefsListener);
+	}
+
+	Table table (Actor... actors) {
+		Table table = new Table(skin);
+		table.defaults().space(6);
+		table.add(actors);
+		return table;
+	}
+
+	void render () {
+		if (viewer.state != null && viewer.state.getCurrent(trackButtons.getCheckedIndex()) == null) {
+			animationList.getSelection().setProgrammaticChangeEvents(false);
+			animationList.setSelected(null);
+			animationList.getSelection().setProgrammaticChangeEvents(true);
+		}
+
+		statusLabel.pack();
+		if (minimizeButton.isChecked())
+			statusLabel.setPosition(10, 25, Align.bottom | Align.left);
+		else
+			statusLabel.setPosition(window.getWidth() + 6, 5, Align.bottom | Align.left);
+
+		stage.act();
+		stage.draw();
+	}
+
+	void toast (String text) {
+		Table table = new Table();
+		table.add(new Label(text, skin));
+		table.getColor().a = 0;
+		table.pack();
+		table.setPosition(-table.getWidth(), -3 - table.getHeight());
+		table.addAction(sequence( //
+			parallel(moveBy(0, table.getHeight(), 0.3f), fadeIn(0.3f)), //
+			delay(5f), //
+			parallel(moveBy(0, table.getHeight(), 0.3f), fadeOut(0.3f)), //
+			removeActor() //
+		));
+		for (Actor actor : toasts.getChildren())
+			actor.addAction(moveBy(0, table.getHeight(), 0.3f));
+		toasts.addActor(table);
+		toasts.getParent().toFront();
+	}
+
+	void savePrefs () {
+		if (!prefsLoaded) return;
+		Preferences prefs = viewer.prefs;
+		prefs.putBoolean("debugBones", debugBonesCheckbox.isChecked());
+		prefs.putBoolean("debugRegions", debugRegionsCheckbox.isChecked());
+		prefs.putBoolean("debugMeshHull", debugMeshHullCheckbox.isChecked());
+		prefs.putBoolean("debugMeshTriangles", debugMeshTrianglesCheckbox.isChecked());
+		prefs.putBoolean("debugPaths", debugPathsCheckbox.isChecked());
+		prefs.putBoolean("debugPoints", debugPointsCheckbox.isChecked());
+		prefs.putBoolean("debugClipping", debugClippingCheckbox.isChecked());
+		prefs.putBoolean("premultiplied", pmaCheckbox.isChecked());
+		prefs.putBoolean("loop", loopCheckbox.isChecked());
+		prefs.putBoolean("add", addCheckbox.isChecked());
+		prefs.putBoolean("holdPrev", holdPrevCheckbox.isChecked());
+		prefs.putBoolean("reverse", reverseCheckbox.isChecked());
+		prefs.putFloat("speed", speedSlider.getValue());
+		prefs.putFloat("mix", mixSlider.getValue());
+		prefs.putFloat("scale", loadScaleSlider.getValue());
+		prefs.putFloat("zoom", zoomSlider.getValue());
+		prefs.putFloat("x", camera.position.x);
+		prefs.putFloat("y", camera.position.y);
+		if (viewer.state != null) {
+			TrackEntry current = viewer.state.getCurrent(0);
+			if (current != null) {
+				String name = current.animation.name;
+				if (name.equals("<empty>")) name = current.next == null ? "" : current.next.animation.name;
+				prefs.putString("animationName", name);
+			}
+		}
+		if (skinList.getSelected() != null) prefs.putString("skinName", skinList.getSelected());
+		prefs.flush();
+	}
+
+	void loadPrefs () {
+		try {
+			Preferences prefs = viewer.prefs;
+			debugBonesCheckbox.setChecked(prefs.getBoolean("debugBones", true));
+			debugRegionsCheckbox.setChecked(prefs.getBoolean("debugRegions", false));
+			debugMeshHullCheckbox.setChecked(prefs.getBoolean("debugMeshHull", false));
+			debugMeshTrianglesCheckbox.setChecked(prefs.getBoolean("debugMeshTriangles", false));
+			debugPathsCheckbox.setChecked(prefs.getBoolean("debugPaths", true));
+			debugPointsCheckbox.setChecked(prefs.getBoolean("debugPoints", true));
+			debugClippingCheckbox.setChecked(prefs.getBoolean("debugClipping", true));
+			pmaCheckbox.setChecked(prefs.getBoolean("premultiplied", true));
+			loopCheckbox.setChecked(prefs.getBoolean("loop", true));
+			addCheckbox.setChecked(prefs.getBoolean("add", false));
+			holdPrevCheckbox.setChecked(prefs.getBoolean("holdPrev", false));
+			reverseCheckbox.setChecked(prefs.getBoolean("reverse", false));
+			speedSlider.setValue(prefs.getFloat("speed", 0.3f));
+			mixSlider.setValue(prefs.getFloat("mix", 0.3f));
+
+			zoomSlider.setValue(prefs.getFloat("zoom", 1));
+			camera.zoom = 1 / prefs.getFloat("zoom", 1);
+			camera.position.x = prefs.getFloat("x", 0);
+			camera.position.y = prefs.getFloat("y", 0);
+
+			loadScaleSlider.setValue(prefs.getFloat("scale", 1));
+			animationList.setSelected(prefs.getString("animationName", null));
+			skinList.setSelected(prefs.getString("skinName", null));
+		} catch (Throwable ex) {
+			System.out.println("Unable to read preferences:");
+			ex.printStackTrace();
+		}
+	}
+}