public class SMBLWSettings {

    //Camera
    public static double cameraSpeed = 20;
    public static double cameraSprintSpeedMultiplier = 5;

    //Sensitivity
    public static double modeMouseSensitivity = 0.2;
    public static double modeMouseShiftSensitivity = 0.02;
    public static double modeMouseWheelSensitivity = 0.001;
    public static double modeMouseWheelShiftSensitivity = 0.0001;
    public static double modeKeyframeMouseSensitivity = 0.002;
    public static double modeKeyframeMouseShiftSensitivity = 0.0002;
    public static double modeKeyframeMouseWheelSensitivity = 0.00001;
    public static double modeKeyframeMouseWheelShiftSensitivity = 0.000001;

    //Rendering
    public static boolean showTextures = true;
    public static boolean isUnlit = false;
    public static boolean showAllWireframes = false;

    //Snapping
    public static double grabSnap = 0.5;
    public static double rotationSnap = 5;
    public static double scaleSnap = 0.5;
    public static float animSnap = 5.0f;
    public static float animSnapShift = 0.1f;

    //UI
    public static boolean showOnScreenCameraControls = false;
    public static boolean showOnScreenInput = false;
    public static boolean showFPSOverlay = false;

} public class Window {

    public static SharedDrawable drawable;
    public static IUIScreen uiScreen;

    public static boolean running = true;

    public static String openGLVersion = "OpenGL hasn't been initialized";

    public static void init() throws LWJGLException, IOException, FontFormatException {
        ResourceManager.preInit();

        Display.setDisplayMode(new org.lwjgl.opengl.DisplayMode(800, 500));
        Display.setResizable(true);
        Display.setTitle(ResourceManager.initResources.getString("smbLevelWorkshop"));
        PixelFormat pxFormat = new PixelFormat().withStencilBits(8).withSamples(4);
        Display.create(pxFormat);
        openGLVersion = GL11.glGetString(GL11.GL_VERSION);
        drawable = new SharedDrawable(Display.getDrawable());

        //Set icon
        LogHelper.info(SMBLevelWorkshop.class, "Setting icon");

        ByteBuffer icon16 = new ImageIOImageData().imageToByteBuffer(ImageIO.read(SMBLevelWorkshop.class.getResourceAsStream("/icon16.png")), false, false, null);
        ByteBuffer icon32 = new ImageIOImageData().imageToByteBuffer(ImageIO.read(SMBLevelWorkshop.class.getResourceAsStream("/icon32.png")), false, false, null);
        ByteBuffer icon128 = new ImageIOImageData().imageToByteBuffer(ImageIO.read(SMBLevelWorkshop.class.getResourceAsStream("/icon128.png")), false, false, null);

        String OS = System.getProperty("os.name").toUpperCase();
        if (OS.contains("WIN")) {
            Display.setIcon(new ByteBuffer[] {icon16, icon32});
        } else if (OS.contains("MAC")) {
            Display.setIcon(new ByteBuffer[] {icon128});
            Application.getApplication().setDockIconImage(new ImageIcon(SMBLevelWorkshop.class.getResource("/icon128.png")).getImage());
        } else {
            Display.setIcon(new ByteBuffer[] {icon32});
        }

        new Thread(() -> {
            try {
                SMBLevelWorkshop.init();
            } catch (LWJGLException | SlickException | FontFormatException | IOException e) {
                CrashHandler.handleCrash(Thread.currentThread(), e);
                System.exit(1);
            }
        }, "initThread").start();

        GL11.glClearColor(33f / 256f, 33f / 256f, 33f / 256f, 1);

        GL11.glEnable(GL11.GL_BLEND);
        GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

        GL11.glEnable(GL11.GL_CULL_FACE); //Cull back faces

        Display.setVSyncEnabled(true); //Enable VSync

        Thread.setDefaultUncaughtExceptionHandler(CrashHandler.UNCAUGHT_EXCEPTION_HANDLER); //Set the uncaught exception handler (Create a crash report)

        while (running && !Display.isCloseRequested()) { //Render loop
            try {
                renderLoop();
            } catch (Exception e) {
                CrashHandler.handleCrash(Thread.currentThread(), e); //Send it to the crash handler
            }
        }

        SMBLevelWorkshop.onQuit();

    }

    private static void renderLoop() {
        setMatrix();

        UIUtils.calcStuff();

        GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

        if (uiScreen != null) {
            Mouse.poll();
            while (Mouse.next()) {
                if (Mouse.getEventButtonState()) {
                    uiScreen.onClick(Mouse.getEventButton(), new PosXY(Mouse.getEventX(), Display.getHeight() - Mouse.getEventY()));
                } else {
                    uiScreen.onClickReleased(Mouse.getEventButton(), new PosXY(Mouse.getEventX(), Display.getHeight() - Mouse.getEventY()));
                }
            }

            Keyboard.poll();
            while (Keyboard.next()) {
                if (Keyboard.getEventKeyState()) {
                    uiScreen.onKeyDown(Keyboard.getEventKey(), Keyboard.getEventCharacter());
                } else {
                    uiScreen.onKeyReleased(Keyboard.getEventKey(), Keyboard.getEventCharacter());
                }
            }

            uiScreen.draw();

        }

        logOpenGLError("End of rendering");

        Display.update();
        Display.sync(60); //Cap to 60 FPS
    }

    public static void setMatrix() {
        GL11.glViewport(0, 0, Display.getWidth(), Display.getHeight());
        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glLoadIdentity();
        GL11.glOrtho(0, Display.getWidth(), Display.getHeight(), 0, 1, -1);
        GL11.glMatrixMode(GL11.GL_MODELVIEW);
    }

    public static void setUIScreen(IUIScreen IUIScreen) {
        Window.uiScreen = IUIScreen;
    }

    public static boolean isCtrlOrCmdDown() {
        return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL) ||
                Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA);
    }

    public static boolean isShiftDown() {
        return Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT);
    }

    public static boolean isAltDown() {
        return Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU);
    }

    public static void logOpenGLError(String location) {
        int error = GL11.glGetError();
        if (error != 0) {
            LogHelper.error(Window.class, "OpenGL Error #" + String.valueOf(error) + " - " + GLU.gluErrorString(error) + " @ " + location);
        }
    }

    public static boolean isModifierKey(int keyCode) {
        switch (keyCode) {
            case 29: //LCONTROL
            case 157: //RCONTROL
            case 56: //LMENU
            case 184: //RMENU
            case 42: //LSHIFT
            case 54: //RSHIFT
            case 219: //LMETA
            case 220: //RMETA
            case 196: //FUNCTION
            case 58: //CAPITAL
                return true;
            default:
                return false;
        }
    }

} @Deprecated
public class AnimData {

    @NotNull protected PosXYZ rotationCenter = new PosXYZ();

    @Deprecated protected TreeMap<Float, Float> posXFrames = new TreeMap<>();
    @Deprecated protected TreeMap<Float, Float> posYFrames = new TreeMap<>();
    @Deprecated protected TreeMap<Float, Float> posZFrames = new TreeMap<>();

    @Deprecated protected TreeMap<Float, Float> rotXFrames = new TreeMap<>();
    @Deprecated protected TreeMap<Float, Float> rotYFrames = new TreeMap<>();
    @Deprecated protected TreeMap<Float, Float> rotZFrames = new TreeMap<>();

    @Deprecated
    public AnimData() {}

    /**
     * @param cad ConfigAnimData (NOT A COPY CONSTRUCTOR! WILL NOT COPY THE CONFIG ANIM DATA!)
     */
    @Deprecated
    public AnimData(ConfigAnimData cad) {
        rotationCenter = new PosXYZ(cad.getRotationCenter().x, cad.getRotationCenter().y, cad.getRotationCenter().z);

        posXFrames = cad.getPosXFrames();
        posYFrames = cad.getPosYFrames();
        posZFrames = cad.getPosZFrames();

        rotXFrames = cad.getRotXFrames();
        rotYFrames = cad.getRotYFrames();
        rotZFrames = cad.getRotZFrames();
    }

    @Deprecated
    public void setRotationCenter(@NotNull PosXYZ rotationCenter) {
        this.rotationCenter = rotationCenter;
    }

    @NotNull
    @Deprecated
    public PosXYZ getRotationCenter() {
        return rotationCenter;
    }

    @Deprecated
    public NamedTransform getNamedTransformAtTime(float time, @Nullable String name) {
        NamedTransform transform = new NamedTransform(name);

        //<editor-fold desc="Pos">
        Map.Entry<Float, Float> floorPosX = posXFrames.floorEntry(time);
        Map.Entry<Float, Float> floorPosY = posYFrames.floorEntry(time);
        Map.Entry<Float, Float> floorPosZ = posZFrames.floorEntry(time);

        Map.Entry<Float, Float> ceilPosX = posXFrames.ceilingEntry(time);
        Map.Entry<Float, Float> ceilPosY = posYFrames.ceilingEntry(time);
        Map.Entry<Float, Float> ceilPosZ = posZFrames.ceilingEntry(time);

        if (floorPosX == null) floorPosX = new QuickMapEntry<>(0.0f, 0.0f);
        if (floorPosY == null) floorPosY = new QuickMapEntry<>(0.0f, 0.0f);
        if (floorPosZ == null) floorPosZ = new QuickMapEntry<>(0.0f, 0.0f);

        if (ceilPosX == null) ceilPosX = floorPosX;
        if (ceilPosY == null) ceilPosY = floorPosY;
        if (ceilPosZ == null) ceilPosZ = floorPosZ;

        float percentPosX = 0.0f;
        float percentPosY = 0.0f;
        float percentPosZ = 0.0f;

        if (ceilPosX.getKey() != 0 && !Objects.equals(floorPosX.getKey(), ceilPosX.getKey())) percentPosX = (time - floorPosX.getKey()) / (ceilPosX.getKey() - floorPosX.getKey());
        if (ceilPosY.getKey() != 0 && !Objects.equals(floorPosY.getKey(), ceilPosY.getKey())) percentPosY = (time - floorPosY.getKey()) / (ceilPosY.getKey() - floorPosY.getKey());
        if (ceilPosZ.getKey() != 0 && !Objects.equals(floorPosZ.getKey(), ceilPosZ.getKey())) percentPosZ = (time - floorPosZ.getKey()) / (ceilPosZ.getKey() - floorPosZ.getKey());

        double posX = MathUtils.lerp(floorPosX.getValue(), ceilPosX.getValue(), MathUtils.cubicEaseInOut(percentPosX));
        double posY = MathUtils.lerp(floorPosY.getValue(), ceilPosY.getValue(), MathUtils.cubicEaseInOut(percentPosY));
        double posZ = MathUtils.lerp(floorPosZ.getValue(), ceilPosZ.getValue(), MathUtils.cubicEaseInOut(percentPosZ));

        transform.setPosition(new PosXYZ(posX, posY, posZ));
        //</editor-fold>

        //<editor-fold desc="Rot">
        Map.Entry<Float, Float> floorRotX = rotXFrames.floorEntry(time);
        Map.Entry<Float, Float> floorRotY = rotYFrames.floorEntry(time);
        Map.Entry<Float, Float> floorRotZ = rotZFrames.floorEntry(time);

        Map.Entry<Float, Float> ceilRotX = rotXFrames.ceilingEntry(time);
        Map.Entry<Float, Float> ceilRotY = rotYFrames.ceilingEntry(time);
        Map.Entry<Float, Float> ceilRotZ = rotZFrames.ceilingEntry(time);

        if (floorRotX == null) floorRotX = new QuickMapEntry<>(0.0f, 0.0f);
        if (floorRotY == null) floorRotY = new QuickMapEntry<>(0.0f, 0.0f);
        if (floorRotZ == null) floorRotZ = new QuickMapEntry<>(0.0f, 0.0f);

        if (ceilRotX == null) ceilRotX = floorRotX;
        if (ceilRotY == null) ceilRotY = floorRotY;
        if (ceilRotZ == null) ceilRotZ = floorRotZ;

        float percentRotX = 0.0f;
        float percentRotY = 0.0f;
        float percentRotZ = 0.0f;

        if (ceilRotX.getKey() != 0 && !Objects.equals(floorRotX.getKey(), ceilRotX.getKey())) percentRotX = (time - floorRotX.getKey()) / (ceilRotX.getKey() - floorRotX.getKey());
        if (ceilRotY.getKey() != 0 && !Objects.equals(floorRotY.getKey(), ceilRotY.getKey())) percentRotY = (time - floorRotY.getKey()) / (ceilRotY.getKey() - floorRotY.getKey());
        if (ceilRotZ.getKey() != 0 && !Objects.equals(floorRotZ.getKey(), ceilRotZ.getKey())) percentRotZ = (time - floorRotZ.getKey()) / (ceilRotZ.getKey() - floorRotZ.getKey());

        double rotX = MathUtils.lerp(floorRotX.getValue(), ceilRotX.getValue(), MathUtils.cubicEaseInOut(percentRotX));
        double rotY = MathUtils.lerp(floorRotY.getValue(), ceilRotY.getValue(), MathUtils.cubicEaseInOut(percentRotY));
        double rotZ = MathUtils.lerp(floorRotZ.getValue(), ceilRotZ.getValue(), MathUtils.cubicEaseInOut(percentRotZ));

        transform.setRotation(new PosXYZ(rotX, rotY, rotZ));
        //</editor-fold>

        return transform;
    }

    //Pos
    @Deprecated
    public void setPosXFrame(float time, float pos) {
        posXFrames.put(time, pos);
    }

    @Deprecated
    public void setPosYFrame(float time, float pos) {
        posYFrames.put(time, pos);
    }

    @Deprecated
    public void setPosZFrame(float time, float pos) {
        posZFrames.put(time, pos);
    }

    @Deprecated
    public void removePosXFrame(float time) {
        posXFrames.remove(time);
    }

    @Deprecated
    public void removePosYFrame(float time) {
        posYFrames.remove(time);
    }

    @Deprecated
    public void removePosZFrame(float time) {
        posZFrames.remove(time);
    }

    @Deprecated
    public TreeMap<Float, Float> getPosXFrames() {
        return posXFrames;
    }

    @Deprecated
    public TreeMap<Float, Float> getPosYFrames() {
        return posYFrames;
    }

    @Deprecated
    public TreeMap<Float, Float> getPosZFrames() {
        return posZFrames;
    }
    
    //Rot
    @Deprecated
    public void setRotXFrame(float time, float rot) {
        rotXFrames.put(time, rot);
    }

    @Deprecated
    public void setRotYFrame(float time, float rot) {
        rotYFrames.put(time, rot);
    }

    @Deprecated
    public void setRotZFrame(float time, float rot) {
        rotZFrames.put(time, rot);
    }

    @Deprecated
    public void removeRotXFrame(float time) {
        rotXFrames.remove(time);
    }

    @Deprecated
    public void removeRotYFrame(float time) {
        rotYFrames.remove(time);
    }

    @Deprecated
    public void removeRotZFrame(float time) {
        rotZFrames.remove(time);
    }

    @Deprecated
    public TreeMap<Float, Float> getRotXFrames() {
        return rotXFrames;
    }

    @Deprecated
    public TreeMap<Float, Float> getRotYFrames() {
        return rotYFrames;
    }

    @Deprecated
    public TreeMap<Float, Float> getRotZFrames() {
        return rotZFrames;
    }

    @Deprecated
    public void mergeWith(AnimData other) {
        posXFrames.putAll(other.posXFrames);
        posYFrames.putAll(other.posYFrames);
        posZFrames.putAll(other.posZFrames);

        rotXFrames.putAll(other.rotXFrames);
        rotYFrames.putAll(other.rotYFrames);
        rotZFrames.putAll(other.rotZFrames);
    }

    @Deprecated
    public AnimData mergeWithCopy(AnimData other) {
        AnimData ad = getCopy();

        ad.posXFrames = new TreeMap<>(posXFrames);
        ad.posXFrames.putAll(other.posXFrames);
        ad.posYFrames = new TreeMap<>(posYFrames);
        ad.posYFrames.putAll(other.posYFrames);
        ad.posZFrames = new TreeMap<>(posZFrames);
        ad.posZFrames.putAll(other.posZFrames);

        ad.rotXFrames = new TreeMap<>(rotXFrames);
        ad.rotXFrames.putAll(other.rotXFrames);
        ad.rotYFrames = new TreeMap<>(rotYFrames);
        ad.rotYFrames.putAll(other.rotYFrames);
        ad.rotZFrames = new TreeMap<>(rotZFrames);
        ad.rotZFrames.putAll(other.rotZFrames);

        return ad;
    }

    @Deprecated
    public void moveFirstFrame(float percent) {
        //Pos
        Map.Entry<Float, Float> firstPosX = posXFrames.firstEntry();
        if (firstPosX != null) {
            posXFrames.remove(firstPosX.getKey());
            posXFrames.put(percent, firstPosX.getValue());
        }

        Map.Entry<Float, Float> firstPosY = posYFrames.firstEntry();
        if (firstPosY != null) {
            posYFrames.remove(firstPosY.getKey());
            posYFrames.put(percent, firstPosY.getValue());
        }

        Map.Entry<Float, Float> firstPosZ = posZFrames.firstEntry();
        if (firstPosZ != null) {
            posXFrames.remove(firstPosZ.getKey());
            posXFrames.put(percent, firstPosZ.getValue());
        }

        //Rot
        Map.Entry<Float, Float> firstRotX = rotXFrames.firstEntry();
        if (firstRotX != null) {
            rotXFrames.remove(firstRotX.getKey());
            rotXFrames.put(percent, firstRotX.getValue());
        }

        Map.Entry<Float, Float> firstRotY = rotYFrames.firstEntry();
        if (firstRotY != null) {
            rotYFrames.remove(firstRotY.getKey());
            rotYFrames.put(percent, firstRotY.getValue());
        }

        Map.Entry<Float, Float> firstRotZ = rotZFrames.firstEntry();
        if (firstRotZ != null) {
            rotXFrames.remove(firstRotZ.getKey());
            rotXFrames.put(percent, firstRotZ.getValue());
        }
    }

    @Deprecated
    public AnimData getCopy() {
        PosXYZ rotationCenterClone = rotationCenter.getCopy();

        TreeMap<Float, Float> posXFramesClone = new TreeMap<>(posXFrames);
        TreeMap<Float, Float> posYFramesClone = new TreeMap<>(posYFrames);
        TreeMap<Float, Float> posZFramesClone = new TreeMap<>(posZFrames);

        TreeMap<Float, Float> rotXFramesClone = new TreeMap<>(rotXFrames);
        TreeMap<Float, Float> rotYFramesClone = new TreeMap<>(rotYFrames);
        TreeMap<Float, Float> rotZFramesClone = new TreeMap<>(rotZFrames);

        AnimData animDataClone = new AnimData();

        animDataClone.rotationCenter = rotationCenterClone;

        animDataClone.posXFrames = posXFramesClone;
        animDataClone.posYFrames = posYFramesClone;
        animDataClone.posZFrames = posZFramesClone;

        animDataClone.rotXFrames = rotXFramesClone;
        animDataClone.rotYFrames = rotYFramesClone;
        animDataClone.rotZFrames = rotZFramesClone;

        return animDataClone;
    }

    @Deprecated
    public void clampKeyframeTimes() {
        NamedTransform time0 = getNamedTransformAtTime(0.0f, null);
        NamedTransform time1 = getNamedTransformAtTime(1.0f, null);

        //Pos
        if (posXFrames.size() > 0 && posXFrames.firstKey() < 0) {
            posXFrames.put(0.0f, (float) time0.getPosition().x);
        }
        if (posYFrames.size() > 0 && posYFrames.firstKey() < 0) {
            posYFrames.put(0.0f, (float) time0.getPosition().y);
        }
        if (posZFrames.size() > 0 && posZFrames.firstKey() < 0) {
            posZFrames.put(0.0f, (float) time0.getPosition().z);
        }

        if (posXFrames.size() > 0 && posXFrames.lastKey() > 1) {
            posXFrames.put(1.0f, (float) time1.getPosition().x);
        }
        if (posYFrames.size() > 0 && posYFrames.lastKey() > 1) {
            posYFrames.put(1.0f, (float) time1.getPosition().y);
        }
        if (posZFrames.size() > 0 && posZFrames.lastKey() > 1) {
            posZFrames.put(1.0f, (float) time1.getPosition().z);
        }
        
        //Rot
        if (rotXFrames.size() > 0 && rotXFrames.firstKey() < 0) {
            rotXFrames.put(0.0f, (float) time0.getRotation().x);
        }
        if (rotYFrames.size() > 0 && rotYFrames.firstKey() < 0) {
            rotYFrames.put(0.0f, (float) time0.getRotation().y);
        }
        if (rotZFrames.size() > 0 && rotZFrames.firstKey() < 0) {
            rotZFrames.put(0.0f, (float) time0.getRotation().z);
        }

        if (rotXFrames.size() > 0 && rotXFrames.lastKey() > 1) {
            rotXFrames.put(1.0f, (float) time1.getRotation().x);
        }
        if (rotYFrames.size() > 0 && rotYFrames.lastKey() > 1) {
            rotYFrames.put(1.0f, (float) time1.getRotation().y);
        }
        if (rotZFrames.size() > 0 && rotZFrames.lastKey() > 1) {
            rotZFrames.put(1.0f, (float) time1.getRotation().z);
        }

        posXFrames.headMap(0.0f, false).clear();
        posYFrames.headMap(0.0f, false).clear();
        posZFrames.headMap(0.0f, false).clear();
        posXFrames.tailMap(1.0f, false).clear();
        posYFrames.tailMap(1.0f, false).clear();
        posZFrames.tailMap(1.0f, false).clear();

        rotXFrames.headMap(0.0f, false).clear();
        rotYFrames.headMap(0.0f, false).clear();
        rotZFrames.headMap(0.0f, false).clear();
        rotXFrames.tailMap(1.0f, false).clear();
        rotYFrames.tailMap(1.0f, false).clear();
        rotZFrames.tailMap(1.0f, false).clear();
    }

} public class BufferedAnimData extends AnimData {

    protected float keyframeBufferTranslation = 0.0f;
    protected float keyframeBufferScale = 1.0f;
    protected float keyframeBufferScaleCenter = 0.0f;

    @Nullable protected Float snapToTranslation = null;

    public TreeMap<Float, Float> getFramesBufferTransformed(TreeMap<Float, Float> original) {
        TreeMap<Float, Float> transformedMap = new TreeMap<>();

        float keyframeBufferTranslationSnapped = getSnappedTranslationValue(keyframeBufferTranslation);

        for (Map.Entry<Float, Float> entry : original.entrySet()) {
            transformedMap.put(
                    ((entry.getKey() + keyframeBufferTranslationSnapped + keyframeBufferScaleCenter) * keyframeBufferScale) - keyframeBufferScaleCenter,
                    entry.getValue());
        }

        return transformedMap;
    }

    public TreeMap<Float, Float> getPosXFramesTransformed() {
        return getFramesBufferTransformed(posXFrames);
    }

    public TreeMap<Float, Float> getPosYFramesTransformed() {
        return getFramesBufferTransformed(posYFrames);
    }

    public TreeMap<Float, Float> getPosZFramesTransformed() {
        return getFramesBufferTransformed(posZFrames);
    }

    public TreeMap<Float, Float> getRotXFramesTransformed() {
        return getFramesBufferTransformed(rotXFrames);
    }

    public TreeMap<Float, Float> getRotYFramesTransformed() {
        return getFramesBufferTransformed(rotYFrames);
    }

    public TreeMap<Float, Float> getRotZFramesTransformed() {
        return getFramesBufferTransformed(rotZFrames);
    }

    public AnimData getTransformedAnimData() {
        AnimData ad = new AnimData();

        ad.setRotationCenter(rotationCenter);

        ad.posXFrames = getPosXFramesTransformed();
        ad.posYFrames = getPosYFramesTransformed();
        ad.posZFrames = getPosZFramesTransformed();

        ad.rotXFrames = getRotXFramesTransformed();
        ad.rotYFrames = getRotYFramesTransformed();
        ad.rotZFrames = getRotZFramesTransformed();

        return ad;
    }

    public float getKeyframeBufferTranslation() {
        return keyframeBufferTranslation;
    }

    public float getKeyframeBufferScale() {
        return keyframeBufferScale;
    }

    public float getKeyframeBufferScaleCenter() {
        return keyframeBufferScaleCenter;
    }

    public void setKeyframeBufferTranslation(float keyframeBufferTranslation) {
        this.keyframeBufferTranslation = keyframeBufferTranslation;
    }

    public void setKeyframeBufferScale(float keyframeBufferScale) {
        this.keyframeBufferScale = keyframeBufferScale;
    }

    public void setKeyframeBufferScaleCenter(float keyframeBufferScaleCenter) {
        this.keyframeBufferScaleCenter = keyframeBufferScaleCenter;
    }

    public void setSnapToTranslation(@Nullable Float snapToTranslation) {
        this.snapToTranslation = snapToTranslation;
    }

    public float getSnappedTranslationValue(float in) {
        if (snapToTranslation == null) {
            return in;
        } else {
            return MathUtils.snapTo(in, snapToTranslation);
        }
    }
} public class ClientLevelData {

    @NotNull private LevelData levelData = new LevelData();

    private Set<String> selectedPlaceables = new HashSet<>();
    @Nullable private UIAction onSelectedPlaceablesChanged;

    private Set<String> selectedObjects = new HashSet<>();
    @Nullable private UIAction onSelectedObjectsChanged;
    private Set<String> hiddenObjects = new HashSet<>();
    @Nullable private UIAction onHiddenObjectsChanged;

    private Set<String> selectedExternalBackgroundObjects = new HashSet<>();
    @Nullable private UIAction onSelectedExternalBackgroundObjectsChanged;

    /**
     * Timeline position as a percentage
     */
    private float timelinePos = 0.0f;
    @Nullable private UIAction1<Float> onTimelinePosChanged;
    private float playbackSpeed = 0.0f;

    public void setLevelData(@NotNull LevelData levelData) {
        this.levelData = levelData;
    }

    @NotNull
    public LevelData getLevelData() {
        return levelData;
    }

    public void addSelectedPlaceable(String name) {
        if (!selectedPlaceables.contains(name)) {
            selectedPlaceables.add(name);
            if (onSelectedPlaceablesChanged != null) {
                onSelectedPlaceablesChanged.execute();
            }
        }
    }

    public void removeSelectedPlaceable(String name) {
        if (selectedPlaceables.contains(name)) {
            selectedPlaceables.remove(name);
            if (onSelectedPlaceablesChanged != null) {
                onSelectedPlaceablesChanged.execute();
            }
        }
    }

    public boolean isPlaceableSelected(String name) {
        return selectedPlaceables.contains(name);
    }

    public void toggleSelectedPlaceable(String name) {
        if (isPlaceableSelected(name)) {
            removeSelectedPlaceable(name);
        } else {
            addSelectedPlaceable(name);
        }
        if (onSelectedPlaceablesChanged != null) {
            onSelectedPlaceablesChanged.execute();
        }
    }

    public void clearSelectedPlaceables() {
        if (selectedPlaceables.size() != 0) {
            selectedPlaceables.clear();
            if (onSelectedPlaceablesChanged != null) {
                onSelectedPlaceablesChanged.execute();
            }
        }
    }

    public Set<String> getSelectedPlaceables() {
        return selectedPlaceables;
    }

    public void setOnSelectedPlaceablesChanged(@Nullable UIAction onSelectedPlaceablesChanged) {
        this.onSelectedPlaceablesChanged = onSelectedPlaceablesChanged;
    }

    public void addSelectedObject(String name) {
        if (!selectedObjects.contains(name)) {
            selectedObjects.add(name);
            if (onSelectedObjectsChanged != null) {
                onSelectedObjectsChanged.execute();
            }
        }
    }

    public void removeSelectedObject(String name) {
        if (selectedObjects.contains(name)) {
            selectedObjects.remove(name);
            if (onSelectedObjectsChanged != null) {
                onSelectedObjectsChanged.execute();
            }
        }
    }

    public boolean isObjectSelected(String name) {
        return selectedObjects.contains(name);
    }

    public void toggleSelectedObject(String name) {
        if (isObjectSelected(name)) {
            removeSelectedObject(name);
        } else {
            addSelectedObject(name);
        }
        if (onSelectedObjectsChanged != null) {
            onSelectedObjectsChanged.execute();
        }
    }

    public void clearSelectedObjects() {
        if (selectedObjects.size() != 0) {
            selectedObjects.clear();
            if (onSelectedObjectsChanged != null) {
                onSelectedObjectsChanged.execute();
            }
        }
    }

    public Set<String> getSelectedObjects() {
        return selectedObjects;
    }

    public void setOnSelectedObjectsChanged(@Nullable UIAction onSelectedObjectsChanged) {
        this.onSelectedObjectsChanged = onSelectedObjectsChanged;
    }

    public void addSelectedExternalBackgroundObject(String name) {
        if (!selectedExternalBackgroundObjects.contains(name)) {
            selectedExternalBackgroundObjects.add(name);
            if (onSelectedExternalBackgroundObjectsChanged != null) {
                onSelectedExternalBackgroundObjectsChanged.execute();
            }
        }
    }

    public void removeSelectedExternalBackgroundObject(String name) {
        if (selectedExternalBackgroundObjects.contains(name)) {
            selectedExternalBackgroundObjects.remove(name);
            if (onSelectedExternalBackgroundObjectsChanged != null) {
                onSelectedExternalBackgroundObjectsChanged.execute();
            }
        }
    }

    public boolean isExternalBackgroundObjectSelected(String name) {
        return selectedExternalBackgroundObjects.contains(name);
    }

    public void toggleSelectedExternalBackgroundObject(String name) {
        if (isExternalBackgroundObjectSelected(name)) {
            removeSelectedExternalBackgroundObject(name);
        } else {
            addSelectedExternalBackgroundObject(name);
        }
        if (onSelectedExternalBackgroundObjectsChanged != null) {
            onSelectedExternalBackgroundObjectsChanged.execute();
        }
    }

    public void clearSelectedExternalBackgroundObjects() {
        if (selectedExternalBackgroundObjects.size() != 0) {
            selectedExternalBackgroundObjects.clear();
            if (onSelectedExternalBackgroundObjectsChanged != null) {
                onSelectedExternalBackgroundObjectsChanged.execute();
            }
        }
    }

    public Set<String> getSelectedExternalBackgroundObjects() {
        return selectedExternalBackgroundObjects;
    }

    public void setOnSelectedExternalBackgroundObjectsChanged(@Nullable UIAction onSelectedExternalBackgroundObjectsChanged) {
        this.onSelectedExternalBackgroundObjectsChanged = onSelectedExternalBackgroundObjectsChanged;
    }

    public void addHiddenObject(String name) {
        if (!hiddenObjects.contains(name)) {
            hiddenObjects.add(name);
            if (onHiddenObjectsChanged != null) {
                onHiddenObjectsChanged.execute();
            }
        }
    }

    public void removeHiddenObject(String name) {
        if (hiddenObjects.contains(name)) {
            hiddenObjects.remove(name);
            if (onHiddenObjectsChanged != null) {
                onHiddenObjectsChanged.execute();
            }
        }
    }

    public boolean isObjectHidden(String name) {
        return hiddenObjects.contains(name);
    }

    public void toggleHiddenObject(String name) {
        if (isObjectHidden(name)) {
            removeHiddenObject(name);
        } else {
            addHiddenObject(name);
        }
        if (onHiddenObjectsChanged != null) {
            onHiddenObjectsChanged.execute();
        }
    }

    public void clearHiddenObjects() {
        if (hiddenObjects.size() != 0) {
            hiddenObjects.clear();
            if (onHiddenObjectsChanged != null) {
                onHiddenObjectsChanged.execute();
            }
        }
    }

    public Set<String> getHiddenObjects() {
        return hiddenObjects;
    }

    public void setOnHiddenObjectsChanged(@Nullable UIAction onHiddenObjectsChanged) {
        this.onHiddenObjectsChanged = onHiddenObjectsChanged;
    }

    public void setTimelinePos(float timelinePos) {
        if (timelinePos != this.timelinePos) {
            this.timelinePos = timelinePos;
            if (onTimelinePosChanged != null) {
                onTimelinePosChanged.execute(timelinePos);
            }
        }
    }

    public void setTimelinePosSeconds(float timelinePos) {
        float newPercent = (timelinePos + levelData.getLeadInTime()) / (levelData.getLeadInTime() + levelData.getMaxTime());
        setTimelinePos(newPercent);
    }

    public float getTimelinePos() {
        return timelinePos;
    }

    public float getTimelinePosSeconds() {
        return timelinePos * (levelData.getLeadInTime() + levelData.getMaxTime()) - levelData.getLeadInTime();
    }

    public float timelinePercentToSeconds(float percent) {
        return (percent * (levelData.getMaxTime() + levelData.getLeadInTime())) - levelData.getLeadInTime();
    }

    public float timelineSecondsToPercent(float seconds) {
        return (seconds + levelData.getLeadInTime()) / (levelData.getMaxTime() + levelData.getLeadInTime());
    }

    public void setOnTimelinePosChanged(@Nullable UIAction1<Float> onTimelinePosChanged) {
        this.onTimelinePosChanged = onTimelinePosChanged;
    }

    public void setPlaybackSpeed(float playbackSpeed) {
        this.playbackSpeed = playbackSpeed;
    }

    public float getPlaybackSpeed() {
        return playbackSpeed;
    }

    public void update(float deltaTime) {
        float newPos = timelinePos + (deltaTime / (levelData.getMaxTime() + levelData.getLeadInTime()) * playbackSpeed);

        if (newPos > 1) {
            setTimelinePos(1);
            setPlaybackSpeed(0);
        } else if (newPos < 0) {
            setTimelinePos(0);
            setPlaybackSpeed(0);
        } else {
            setTimelinePos(newPos);
        }
    }

    public void clearSelectedKeyframes() {
        //TODO
    }

} public class ProjectManager {

    private static Project currentProject;

    public static void setCurrentProject(Project currentProject) {
        ProjectManager.currentProject = currentProject;
    }

    public static Project getCurrentProject() {
        return currentProject;
    }

    public static LevelData getCurrentLevelData() {
        return getCurrentProject().clientLevelData.getLevelData();
    }

    public static ClientLevelData getCurrentClientLevelData() {
        return getCurrentProject().clientLevelData;
    }

} public class LangManager {

    //                         <Locale,    <Key   , Value >>
    @NotNull private static Map<String, Map<String, String>> localeMap = new HashMap<>();
    @NotNull private static String selectedLang = "en_US";

    /**
     * Add an item given a locale, key and value
     *
     * @param locale The language locale (eg: en_US)
     * @param key The identifier
     * @param value The translated string
     */
    public static void addItem(String locale, String key, String value) {
        if (localeMap.containsKey(locale)) {
            Map<String, String> langMap = localeMap.get(locale);
            langMap.put(key, value);
            localeMap.put(locale, langMap);
        } else {
            Map<String, String> langMap = new HashMap<>();
            langMap.put(key, value);
            localeMap.put(locale, langMap);
        }
    }

    /**
     * Get a translated string, given a locale and key
     *
     * @param locale The language locale (eg: en_US)
     * @param key The identifier
     * @return The translated string, or the key if the entry wasn't found
     */
    public static String getItem(String locale, String key) {
        if (localeMap.containsKey(locale)) {
            if (localeMap.get(locale).containsKey(key)) {
                return localeMap.get(locale).get(key);
            } else {
                return key;
            }
        } else {
            return key;
        }
    }

    public static String getItem(String key) {
        return getItem(selectedLang, key);
    }

    public static void setSelectedLang(@NotNull String selectedLang) {
        LangManager.selectedLang = selectedLang;
    }

    @NotNull
    public static String getSelectedLang() {
        return selectedLang;
    }

} public class ResourceManager {

    public static ResourceBundle initResources;
    private static Map<String, IResource> resources = new HashMap<>();
    private static Map<String, ResourceTexture> textureResources = new HashMap<>(); //This exists to prevent casting from IResource to ResourceTexture

    private static final String vanillaResourcePath = "resources/vanilla";

    private static Map<String, File> fontResourcesToLoad = new HashMap<>(); //TTF font
    private static Map<String, File> fontCacheResourcesToLoad = new HashMap<>(); //JSON font cache
    private static Map<String, File> pngResourcesToLoad = new HashMap<>(); //PNG textures
    private static Map<String, File> langPackResourcesToLoad = new HashMap<>(); //XML language pack
    private static Map<String, File> vertShaderResourcesToLoad = new HashMap<>(); //Vertex shader
    private static Map<String, File> fragShaderResourcesToLoad = new HashMap<>(); //Fragment shader
    private static Map<String, File> shaderCacheResourcesToLoad = new HashMap<>(); //JSON shader cache
//    private static Map<String, URL> musicOggResourcesToLoad = new HashMap<>(); //OGG Music Audio
    private static Map<String, File> objResourcesToLoad = new HashMap<>(); //OBJ models

    private static Map<String, String> erroredResources = new HashMap<>();

    @Nullable private static File tempDir;

    public static void preInit() throws IOException, FontFormatException {
        initResources = ResourceBundle.getBundle("initResources");
    }

    public static void queueVanillaResources() {
        try {
            tempDir = File.createTempFile("SMBLevelWorkshop", null);
            tempDir.deleteOnExit();
            if (!tempDir.delete()) {
                throw new IOException("Could not delete temp file: " + tempDir.getAbsolutePath());
            }
            if (!tempDir.mkdir()) {
                throw new IOException("Could not create temp directory: " + tempDir.getAbsolutePath());
            }

            final File jarFile = new File(ResourceManager.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());

            if (!jarFile.isDirectory()) { //Run from JAR

                final JarFile jar = new JarFile(jarFile);
                final Enumeration<JarEntry> enumEntries = jar.entries();

                while (enumEntries.hasMoreElements()) { //Iterate through all resources
                    JarEntry file = enumEntries.nextElement();

                    if (!file.toString().startsWith(vanillaResourcePath) || file.isDirectory()) {
                        continue;
                    }

                    LogHelper.trace(ResourceManager.class, String.format("Found resource \"%s\"",
                            file.toString().substring(vanillaResourcePath.length() + 1)));


//                    if (file.toString().toUpperCase().endsWith(".BGMUSIC.OGG")) { //Check for ogg stuff here as it can't use a temp file
//                        musicOggResourcesToLoad.put(file.toString().substring(vanillaResourcePath.length() + 1),
//                                ResourceManager.class.getResource("/" + file.toString()));
//                    } else {

                    File outFile = new File(tempDir, file.toString());
                    outFile.deleteOnExit();

                    if (!outFile.getParentFile().exists()) {
                        if (!outFile.getParentFile().mkdirs()) {
                            throw new IOException("Error while creating directories for file: " + outFile.getAbsolutePath());
                        }
                    }
                    if (!outFile.createNewFile()) {
                        throw new IOException("Error while creating new file: " + outFile.getAbsolutePath());
                    }

                    Files.copy(ResourceManager.class.getResourceAsStream("/" + file.toString()), outFile.toPath(), StandardCopyOption.REPLACE_EXISTING);

                    queueAddResource(new File(tempDir, file.toString()), file.toString().substring(vanillaResourcePath.length() + 1));
//                    }

                }

            } else { //Run from outside of JAR (Eg: IDE)

                final URL url = ResourceManager.class.getResource("/" + vanillaResourcePath);

                if (url != null) {

                    Files.walkFileTree(new File(url.getPath()).toPath(), new FileVisitor<Path>() {
                        @Override
                        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

                            if (!file.toString().startsWith(url.getFile())) {
                                return FileVisitResult.CONTINUE;
                            }

                            LogHelper.trace(ResourceManager.class, String.format("Found resource \"%s\"", file.toString().substring(url.getFile().length() + 1)));

                            queueAddResource(file.toFile(), file.toString().substring(url.getFile().length() + 1));

                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                            LogHelper.fatal(getClass(), exc);
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                            return FileVisitResult.CONTINUE;
                        }
                    });
                }

            }

        } catch (IOException | URISyntaxException e) {
            e.printStackTrace();
        }

    }

    /**
     * Will queue a resource to be added later by {@link ResourceManager#registerAllResources()}
     *
     * @param resourceFile The file of the resource to be queued
     * @param resourceID The name, or ID of the resource to be queued. This is typically the name of the file (with file type extension)
     * @throws IOException
     */
    private static void queueAddResource(@NotNull File resourceFile, @NotNull String resourceID) throws IOException {

        if (resourceID.toUpperCase().endsWith(".PNG")) { //PNG texture (Ignore .exclude.png)
            pngResourcesToLoad.put(resourceID, resourceFile);
        } else if (resourceID.toUpperCase().endsWith(".TTF")) { //TTF Font
            fontResourcesToLoad.put(resourceID, resourceFile);
        } else if (resourceID.toUpperCase().endsWith(".FONTS.JSON")) { //Fonts to add to the Font cache
            fontCacheResourcesToLoad.put(resourceID, resourceFile);
        } else if (resourceID.toUpperCase().endsWith(".LANG.XML")) { //Language packs
            langPackResourcesToLoad.put(resourceID, resourceFile);
        } else if (resourceID.toUpperCase().endsWith(".FRAG")) { //Fragment shader
            fragShaderResourcesToLoad.put(resourceID, resourceFile);
        } else if (resourceID.toUpperCase().endsWith(".VERT")) { //Vertex shader
            vertShaderResourcesToLoad.put(resourceID, resourceFile);
        } else if (resourceID.toUpperCase().endsWith(".SHADERS.JSON")) { //Shaders to add to the Shader cache
            shaderCacheResourcesToLoad.put(resourceID, resourceFile);
//        } else if (resourceID.toUpperCase().endsWith(".BGMUSIC.OGG")) { //OGG Background Music
//            musicOggResourcesToLoad.put(resourceID, resourceFile.toURI().toURL());
        } else if (resourceID.toUpperCase().endsWith(".OBJ")) { //Shaders to add to the Shader cache
            objResourcesToLoad.put(resourceID, resourceFile);
        } else if (!Objects.equals(resourceID.toUpperCase(), ".DS_STORE") && //Ignore .DS_STORE
                !Objects.equals(resourceID.toUpperCase(), "THUMBS.DB") && //Ignore THUMBS.DB
                !resourceID.toUpperCase().endsWith(".MTL")) { //Ignore .MTL (These are handled by the OBJLoader))
            addWarnedResource(resourceID, initResources.getString("unrecognisedFileExtension"));
            LogHelper.warn(ResourceManager.class, String.format("Unrecognised resource file extension: \"%s\"", resourceID));
        }

    }

    /**
     * Will register all resources in the correct order.
     */
    public static void registerAllResources() {
        erroredResources.clear();

        //The total number of resources to load
        int totalResources =
                fontResourcesToLoad.size() +
                fontCacheResourcesToLoad.size() +
                pngResourcesToLoad.size() +
                langPackResourcesToLoad.size() +
                vertShaderResourcesToLoad.size() +
                fragShaderResourcesToLoad.size() +
                shaderCacheResourcesToLoad.size() +
//                musicOggResourcesToLoad.size();
                objResourcesToLoad.size();
        int currentResource = 0;

        for (Map.Entry<String, File> resource : fontResourcesToLoad.entrySet()) { //Register Font TTF files
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerTTF(resource.getValue(), resource.getKey());
            } catch (IOException e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }
        }

        for (Map.Entry<String, File> resource : fontCacheResourcesToLoad.entrySet()) { //Register Font cache JSON files
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerFontCache(resource.getValue(), resource.getKey());
            } catch (IOException | LWJGLException | SlickException | FontFormatException | ParseException e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }
        }

        //Load and register the loading screen background first if it exists
        File loadBGFile = pngResourcesToLoad.get("_loadBackground.png");
        if (loadBGFile != null) {
            currentResource++;
            LoadingScreen.infoMessage = "_loadBackground.png";
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerPNG(loadBGFile, "_loadBackground.png");
            } catch (Exception e) {
                addErroredResource("_loadBackground.png", e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }

            pngResourcesToLoad.remove("_loadBackground.png");
        }

        for (Map.Entry<String, File> resource : pngResourcesToLoad.entrySet()) { //Register PNG textures
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerPNG(resource.getValue(), resource.getKey());
            } catch (Exception e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }
        }

        for (Map.Entry<String, File> resource : langPackResourcesToLoad.entrySet()) { //Register Lang files
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerLangPack(resource.getValue(), resource.getKey());
            } catch (ParserConfigurationException | IOException | SAXException e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, e);
                e.printStackTrace();
            }
        }

        for (Map.Entry<String, File> resource : vertShaderResourcesToLoad.entrySet()) { //Register Vertex shaders
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerShader(resource.getValue(), resource.getKey(), GL20.GL_VERTEX_SHADER);
            } catch (IOException | LWJGLException | GLSLCompileException e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }
        }

        for (Map.Entry<String, File> resource : fragShaderResourcesToLoad.entrySet()) { //Register Fragment shaders
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerShader(resource.getValue(), resource.getKey(), GL20.GL_FRAGMENT_SHADER);
            } catch (IOException | LWJGLException | GLSLCompileException e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }
        }

        for (Map.Entry<String, File> resource : shaderCacheResourcesToLoad.entrySet()) { //Register Shader cache JSON files
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerShaderProgram(resource.getValue(), resource.getKey());
            } catch (Exception e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }
        }

//        for (Map.Entry<String, URL> resource : musicOggResourcesToLoad.entrySet()) { //Register OGG background music
//            currentResource++;
//            LoadingScreen.infoMessage = resource.getKey();
//            LoadingScreen.progress = currentResource / (double) totalResources;
//            try {
//                registerOggMusic(resource.getValue(), resource.getKey());
//            } catch (Exception e) {
//                addErroredResource(resource.getKey(), e.getMessage());
//                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
//            }
//        }

        for (Map.Entry<String, File> resource : objResourcesToLoad.entrySet()) { //Register Shader cache JSON files
            currentResource++;
            LoadingScreen.infoMessage = resource.getKey();
            LoadingScreen.progress = currentResource / (double) totalResources;
            try {
                registerObj(resource.getValue(), resource.getKey());
            } catch (Exception e) {
                addErroredResource(resource.getKey(), e.getMessage());
                LogHelper.error(ResourceManager.class, CrashHandler.getStackTraceString(e));
            }
        }

        if (tempDir != null) {
            try {
                removeRecursive(tempDir.toPath());
            } catch (IOException e) {
                LogHelper.error(ResourceManager.class, "Failed to delete temp dir: " + tempDir.getAbsolutePath());
                LogHelper.error(ResourceManager.class, e);
            }
        }

    }

    /**
     * Will load and register a PNG texture
     *
     * @param resourceFile The file to be registered
     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file (with .png extension)
     * @throws Exception
     */
    public static void registerPNG(@NotNull File resourceFile, @NotNull String resourceID) throws Exception {
        if (!resourceID.toUpperCase().endsWith(".EXCLUDE.PNG")) {
            LogHelper.trace(ResourceManager.class, String.format("Loading PNG texture from \"%s\"", resourceFile.getPath()));

            ResourceTexture tex = new ResourceTexture("PNG", resourceFile);
            textureResources.put(resourceID.substring(0, resourceID.length() - 4), tex);

            LogHelper.trace(ResourceManager.class, String.format("Added PNG texture \"%s\"", resourceID.substring(0, resourceID.length() - 4)));
        } else {
            LogHelper.trace(ResourceManager.class, String.format("Ignoring PNG texture \"%s\"", resourceID.substring(0, resourceID.length() - 4)));
        }
    }

    /**
     * Will load and register a TrueType font
     *
     * @param resourceFile The file to be registered
     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file (with .ttf extension)
     * @throws IOException
     */
    public static void registerTTF(@NotNull File resourceFile, @NotNull String resourceID) throws IOException {
        LogHelper.trace(ResourceManager.class, String.format("Loading TTF font data from \"%s\"", resourceFile.getPath()));

        try {
            FontCache.registerAWTFont(resourceID.substring(0, resourceID.length() - 4), new FileInputStream(resourceFile));
            LogHelper.trace(ResourceManager.class, String.format("Added TTF font data \"%s\"", resourceID.substring(0, resourceID.length() - 4)));
        } catch (FontFormatException e) {
            addErroredResource(resourceID, e.getMessage());
            LogHelper.error(ResourceManager.class, String.format("Invalid font format \"%s\"", resourceFile.getPath()));
            LogHelper.error(ResourceManager.class, e);
        }
    }

    /**
     * Will load and register fonts and their sizes specified within the json file
     *
     * @param resourceFile The file to be registered
     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file (with .fonts.json extension)
     * @throws IOException
     * @throws ParseException
     * @throws LWJGLException
     * @throws SlickException
     * @throws FontFormatException
     */
    public static void registerFontCache(File resourceFile, @NotNull String resourceID) throws IOException, ParseException, LWJGLException, SlickException, FontFormatException {
        LogHelper.trace(ResourceManager.class, String.format("Loading font JSON data from \"%s\"", resourceFile.getPath()));

        JSONParser parser = new JSONParser();
        Object obj = parser.parse(new FileReader(Objects.requireNonNull(resourceFile)));

        if (obj instanceof JSONArray) {
            JSONArray unicodeFonts = (JSONArray) obj;

            for (Object fontObject : unicodeFonts) {
                JSONObject fontData = (JSONObject) fontObject;

                if (fontData.containsKey("name") && fontData.containsKey("size")) {
                    if (fontData.get("name") instanceof String) {
                        if (fontData.get("size") instanceof Long) {

                            String name = (String) fontData.get("name");
                            int size = (int) (long) fontData.get("size");

                            craftedcart.smblevelworkshop.Window.drawable.makeCurrent();
                            FontCache.registerUnicodeFont(name, size); //Register the font

                        } else {
                            addErroredResource(resourceID, String.format(initResources.getString("errorFontSizeNotNumber"), fontData.toJSONString()));
                            LogHelper.error(ResourceManager.class, String.format("Invalid fonts definition \"%s\" - Entry \"size\" of \"%s\" is not a number",
                                    resourceID, fontData.toJSONString()));
                        }
                    } else {
                        addErroredResource(resourceID, String.format(initResources.getString("errorFontNameNotString"), fontData.toJSONString()));
                        LogHelper.error(ResourceManager.class, String.format("Invalid fonts definition \"%s\" - Entry \"name\" of \"%s\" is not a string",
                                resourceID, fontData.toJSONString()));
                    }
                } else {
                    addErroredResource(resourceID, String.format(initResources.getString("errorFontDefMissingKeys"), fontData.toJSONString()));
                    LogHelper.error(ResourceManager.class, String.format("Invalid fonts definition \"%s\" - Entry \"%s\" does not contain the keys \"name\" and \"size\"",
                            resourceID, fontData.toJSONString()));
                }
            }

        } else {
            addErroredResource(resourceID, initResources.getString("errorFontDefNotArray"));
            LogHelper.error(ResourceManager.class, String.format("Invalid fonts definition \"%s\" - File contents is not an array", resourceID));
        }

        LogHelper.trace(ResourceManager.class, String.format("Added font JSON data \"%s\"", resourceID.substring(0, resourceID.length() - 11)));
    }

    /**
     * Will load and register an XML language pack
     *
     * @param resourceFile The file to be registered
     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file (with .lang.xml extension)
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     */
    public static void registerLangPack(@NotNull File resourceFile, @NotNull String resourceID) throws ParserConfigurationException, IOException, SAXException {
        LogHelper.trace(ResourceManager.class, String.format("Loading XML lang data from \"%s\"", resourceFile.getPath()));

        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        Document document = builder.parse(resourceFile); //Parse the XML file

        Element root = document.getDocumentElement();
        if (!Objects.equals(root.getTagName(), "langPack")) { //The root tag is not "langPack"
            addErroredResource(resourceID, initResources.getString("errorLangNotLangPack"));
            LogHelper.error(ResourceManager.class, String.format("The root tag of \"%s\" is not \"langPack\"", resourceID));
            return;
        }

        String locale = root.getAttribute("locale");
        if (locale.isEmpty()) { //Error: No locale specified
            addErroredResource(resourceID, initResources.getString("errorLangNoLocale"));
            LogHelper.error(ResourceManager.class, String.format("No locale attribute specified for \"%s\"", resourceID));
            return;
        }

        for (int i = 0; i < root.getChildNodes().getLength(); i++) {
            Node child = root.getChildNodes().item(i);
            if (Objects.equals(child.getNodeName(), "entry")) {

                @Nullable Node key = child.getAttributes().getNamedItem("key");
                @Nullable Node val = child.getAttributes().getNamedItem("val");

                if (key != null && val != null) {
                    LangManager.addItem(locale, key.getTextContent(), val.getTextContent());
                } else {
                    addErroredResource(resourceID, initResources.getString("errorLangMissingKeyVal"));
                    LogHelper.error(ResourceManager.class, String.format("XML node entry in \"%s\" missing attributes \"key\" and \"val\"", resourceID));
                }

            }
        }

        LogHelper.trace(ResourceManager.class, String.format("Added XML lang data \"%s\"", resourceID.substring(0, resourceID.length() - 4)));
    }

    /**
     * Will load and compile a GLSL shader
     *
     * @param resourceFile The file to be compuled
     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file (with .frag or .vert extension)
     * @param shaderType {@link GL20#GL_FRAGMENT_SHADER} or {@link GL20#GL_VERTEX_SHADER}
     * @throws IOException
     * @throws LWJGLException
     */
    public static void registerShader(@NotNull File resourceFile, @NotNull String resourceID, int shaderType) throws IOException, LWJGLException, GLSLCompileException {
        LogHelper.trace(ResourceManager.class, String.format("Loading GLSL shader from \"%s\"", resourceFile.getPath()));

        ResourceShader tex = new ResourceShader(shaderType, resourceFile);
        resources.put(resourceID.substring(0, resourceID.length() - 5), tex);

        LogHelper.trace(ResourceManager.class, String.format("Added GLSL shader \"%s\"", resourceID.substring(0, resourceID.length() - 5)));
    }

    /**
     * Will load shader programs from a vert and frag shader resource specified within the json file
     *
     * @param resourceFile The file to be registered
     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file
     */
    public static void registerShaderProgram(@NotNull File resourceFile, @NotNull String resourceID) throws Exception {
        LogHelper.trace(ResourceManager.class, String.format("Loading shader program JSON data from \"%s\"", resourceFile.getPath()));

        JSONParser parser = new JSONParser();
        Object obj = parser.parse(new FileReader(resourceFile));

        if (obj instanceof JSONArray) {
            JSONArray programs = (JSONArray) obj;

            for (Object programObject : programs) {
                JSONObject programData = (JSONObject) programObject;

                if (programData.containsKey("name") && programData.containsKey("vert") && programData.containsKey("frag")) {
                    if (programData.get("name") instanceof String && programData.get("vert") instanceof String && programData.get("frag") instanceof String) {

                        String name = (String) programData.get("name");
                        String vert = (String) programData.get("vert");
                        String frag = (String) programData.get("frag");

                        ResourceShaderProgram shaderProgram = new ResourceShaderProgram(getShader(vert), getShader(frag));
                        resources.put(name, shaderProgram);

                    } else {
                        addErroredResource(resourceID, String.format(initResources.getString("errorShaderProgEntriesNotString"), programData.toJSONString()));
                        LogHelper.error(ResourceManager.class, String.format("Invalid shader program definition \"%s\" - Entry \"name\", \"vert\" and / or " +
                                "\"frag\" of \"%s\" is not a string",
                                resourceID, programData.toJSONString()));
                    }
                } else {
                    addErroredResource(resourceID, String.format(initResources.getString("errorShaderProgDefMissingKeys"), programData.toJSONString()));
                    LogHelper.error(ResourceManager.class, String.format("Invalid shader program definition \"%s\" - Entry \"%s\" does not contain the keys \"name\", \"vert\" and \"frag\"",
                            resourceID, programData.toJSONString()));
                }
            }

        } else {
            addErroredResource(resourceID, initResources.getString("errorShaderProgDefNotArray"));
            LogHelper.error(ResourceManager.class, String.format("Invalid shader program definition \"%s\" - File contents is not an array", resourceID));
        }

        LogHelper.trace(ResourceManager.class, String.format("Added shader program JSON data \"%s\"", resourceID.substring(0, resourceID.length() - 13)));
    }

//    /**
//     * Will load OGG music files
//     *
//     * @param resourceFile The file to be registered
//     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file (with .bgMusic.ogg extension)
//     */
//    public static void registerOggMusic(@NotNull URL resourceFile, @NotNull String resourceID) throws Exception {
//        LogHelper.trace(ResourceManager.class, String.format("Loading OGG music data from \"%s\"", resourceFile.getPath()));
//
//        AudioUtils.addMusic(resourceID, resourceFile);
//
//        LogHelper.trace(ResourceManager.class, String.format("Added OGG music data \"%s\"", resourceID.substring(0, resourceID.length() - 12)));
//    }

    /**
     * Will load obj models
     *
     * @param resourceFile The file to be registered
     * @param resourceID The name, or ID of the resource to be registered. This is typically the name of the file (with .obj extension)
     */
    public static void registerObj(@NotNull File resourceFile, @NotNull String resourceID) throws Exception {
        LogHelper.trace(ResourceManager.class, String.format("Loading OBJ model data from \"%s\"", resourceFile.getPath()));

        resources.put(resourceID.substring(0, resourceID.length() - 4), OBJLoader.loadModel(resourceFile.getPath()));

        LogHelper.trace(ResourceManager.class, String.format("Added OBJ model data \"%s\"", resourceID.substring(0, resourceID.length() - 4)));
    }

    private static void addErroredResource(@NotNull String key, @NotNull String value) {
        erroredResources.put(key, value);
        if (LoadingScreen.debugMessagesOverlay == null) {
            LoadingScreen.debugMessagesOverlay = new LinkedHashMap<>();
        }
        //noinspection ConstantConditions
        LoadingScreen.debugMessagesOverlay.put(String.format("%s: %s", key, value), UIColor.matRed());
    }

    private static void addWarnedResource(String key, String value) {
        erroredResources.put(key, value);
        if (LoadingScreen.debugMessagesOverlay == null) {
            LoadingScreen.debugMessagesOverlay = new LinkedHashMap<>();
        }
        //noinspection ConstantConditions
        LoadingScreen.debugMessagesOverlay.put(String.format("%s: %s", key, value), UIColor.matYellow());
    }

    public static boolean doesTextureResourceExist(String resourceID) {
        return textureResources.containsKey(resourceID);
    }

    public static boolean doesResourceExist(String resourceID) {
        return resources.containsKey(resourceID);
    }

    public static IResource getResource(String resourceID) {
        return getResource(resourceID, true);
    }

    public static IResource getResource(String resourceID, boolean showErrorIfNonExistent) {
        if (resources.containsKey(resourceID)) {
            return resources.get(resourceID);
        } else if (showErrorIfNonExistent) {
            LogHelper.error(ResourceManager.class, String.format("Resource \"%s\" does not exist", resourceID));
        }
        return null;
    }

    public static ResourceTexture getTexture(String resourceID) {
        return getTexture(resourceID, true);
    }

    public static ResourceTexture getTexture(String resourceID, boolean showErrorIfNonExistent) {
        if (textureResources.containsKey(resourceID)) {
            return textureResources.get(resourceID);
        } else if (showErrorIfNonExistent) {
            LogHelper.error(ResourceManager.class, String.format("Texture resource \"%s\" does not exist", resourceID));
        }
        return null;
    }

    public static ResourceShader getShader(String resourceID) {
        return (ResourceShader) getResource(resourceID);
    }

    public static ResourceShaderProgram getShaderProgram(String resourceID) {
        return (ResourceShaderProgram) getResource(resourceID);
    }

    public static ResourceModel getModel(String resourceID) {
        return (ResourceModel) getResource(resourceID);
    }

    private static File getAppSupportDirectory() {

        String workingDirectory;
        String os = (System.getProperty("os.name")).toUpperCase();

        if (os.contains("WIN")) {
            //If on Windows, get the Application Data folder
            workingDirectory = System.getenv("AppData") + "\\SMBLevelWorkshop";
        } else if (os.contains("MAC OS X")) {
            //If we are on a Mac, goto the "Application Support" directory
            workingDirectory = System.getProperty("user.home") + "/Library/Application Support/SMBLevelWorkshop";
        } else {
            //Assume Linux or some other distro. Use the User's home folder
            workingDirectory = System.getProperty("user.home") + "/.config/SMBLevelWorkshop";
        }

        File fileAppSupportDirectory = new File(workingDirectory);

        tryCreateDirectory(fileAppSupportDirectory);
        return fileAppSupportDirectory;

    }

    private static void tryCreateDirectory(File directory) {
        if ((!directory.isDirectory() && directory.mkdirs()) || directory.isDirectory()) {
            return;
        }
        LogHelper.fatal(ResourceManager.class, String.format("Failed to create directories \"%s\"", directory.toString()));
        throw new RuntimeException(String.format("Failed to create directories \"%s\"", directory.toString()));
    }

    public static void removeRecursive(Path path) throws IOException
    {
        Files.walkFileTree(path, new SimpleFileVisitor<Path>()
        {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException
            {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException
            {
                // try to delete the file anyway, even if its attributes
                // could not be read, since delete-only access is
                // theoretically possible
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException
            {
                if (exc == null)
                {
                    Files.delete(dir);
                    return FileVisitResult.CONTINUE;
                }
                else
                {
                    // directory iteration failed; propagate exception
                    throw exc;
                }
            }
        });
    }

} public class DefaultUITheme extends UITheme {

    public DefaultUITheme() {
        labelFont = FontCache.getUnicodeFont("Roboto-Regular", 16);
        headerFont = FontCache.getUnicodeFont("Roboto-Regular", 24);
        labelTextColor = UIColor.matWhite();
        scrollbarThickness = 2;
        checkBoxUncheckedColor = UIColor.matWhite();
    }

} public class MainScreen extends FluidUIScreen {

    private static final double TIMELINE_HEIGHT = 136;

    //Camera
    @NotNull private PosXYZ cameraPos = new PosXYZ(5, 5, 5);
    @NotNull private PosXY cameraRot = new PosXY(-45, 35);

    //UI
    private final Image modeCursor = new Image();
    private final Component mainUI = new Component();
    private final Label modeLabel = new Label();
    private final Label modeDirectionLabel = new Label();
    private final Panel notifPanel = new Panel();
    private final Timeline timeline = new Timeline(this);
    private final Panel onScreenCameraControlsPanel = new Panel();
    private final FPSOverlay fpsOverlay = new FPSOverlay();

    private EnumObjectMode objectMode = EnumObjectMode.PLACEABLE_EDIT;

    //UI: Left Panel
    public final Panel addPlaceablePanel = new Panel();
    public final Panel outlinerPlaceablesPanel = new Panel();
    public final ListBox outlinerPlaceablesListBox = new ListBox();
    public final Panel outlinerObjectsPanel = new Panel();
    public final ListBox outlinerObjectsListBox = new ListBox();

    private final TextButton importObjButton = new TextButton();
    private final TextButton importConfigButton = new TextButton();
    private final TextButton exportButton = new TextButton();
    private final TextButton settingsButton = new TextButton();
    private final TextButton projectSettingsButton = new TextButton();

    //UI: Placeable Properties
    private final ListBox propertiesPlaceablesListBox = new ListBox();
    private final ListBox propertiesObjectsListBox = new ListBox();

    private final PlaceableScaleTextFields placeableScaleTextFields = new PlaceableScaleTextFields(this, null);
    private final PlaceableRotationTextFields placeableRotationTextFields = new PlaceableRotationTextFields(this, placeableScaleTextFields.getFirstTextField());
    private final PlaceablePositionTextFields placeablePositionTextFields = new PlaceablePositionTextFields(this, placeableRotationTextFields.getFirstTextField());

    private final TextButton typeButton = new TextButton();
    @Nullable private List<String> typeList = null;

    private final TextButton placeableItemGroupButton = new TextButton();

    //UI: Object Properties
    private final TextButton objectItemGroupButton = new TextButton();

    //UI: Text Fields
    private Set<TextField> textFields;

    //UI: Input Overlay
    private final InputOverlay inputOverlay = new InputOverlay();

    //Undo
    @NotNull private List<UndoCommand> undoCommandList = new ArrayList<>();
    @NotNull private List<UndoCommand> redoCommandList = new ArrayList<>();

    private boolean preventRendering = false; //Used when unloading textures and VBOs
    private boolean isLoadingProject = false; //Just in case disabling the button is too slow

    //Notifications
    private int notificationID = 0;

    //Mouse & Scroll Wheel Delta (For snapping)
    private double deltaX = 0;

    //Locks
    private final Object outlinerPlaceablesListBoxLock = new Object();
    private final Object outlinerObjectsListBoxLock = new Object();
    private final Object renderingLock = new Object();

    private final Set<UIAction> nextFrameActions = new HashSet<>();

    public MainScreen() {
        init();
    }

    private void init() {

        if (textFields == null) {
            textFields = new HashSet<>();
        }

        try {
            Window.drawable.makeCurrent();
        } catch (LWJGLException e) {
            e.printStackTrace();
        }

        //Defined at class level
        modeCursor.setOnInitAction(() -> {
            modeCursor.setTopLeftPos(-16, -16);
            modeCursor.setBottomRightPos(16, 16);
            modeCursor.setTopLeftAnchor(0.5, 0.5);
            modeCursor.setBottomRightAnchor(0.5, 0.5);
            modeCursor.setVisible(false);
            modeCursor.setTexture(ResourceManager.getTexture("image/modeEditCursor").getTexture());
            modeCursor.setColor(UIColor.matGreen());
        });
        addChildComponent("modeCursor", modeCursor);

        //Defined at class level
        mainUI.setOnInitAction(() -> {
            mainUI.setTopLeftPos(0, 0);
            mainUI.setBottomRightPos(0, 0);
            mainUI.setTopLeftAnchor(0, 0);
            mainUI.setBottomRightAnchor(1, 1);
            mainUI.setTheme(new DefaultUITheme());
        });
        addChildComponent("mainUI", mainUI);

        //<editor-fold desc="Bottom Panel">
        final Panel bottomPanel = new Panel();
        bottomPanel.setOnInitAction(() -> {
            bottomPanel.setBackgroundColor(UIColor.matGrey900(0.75));
            bottomPanel.setTopLeftPos(260, -50 - TIMELINE_HEIGHT);
            bottomPanel.setBottomRightPos(-260, -4 - TIMELINE_HEIGHT);
            bottomPanel.setTopLeftAnchor(0, 1);
            bottomPanel.setBottomRightAnchor(1, 1);
        });
        mainUI.addChildComponent("bottomPanel", bottomPanel);
        //</editor-fold>

        //<editor-fold desc="ProjectManager.getCurrentProject().mode Label">
        //Defined at class level
        modeLabel.setOnInitAction(() -> {
            modeLabel.setTopLeftPos(4, 0);
            modeLabel.setBottomRightPos(-4, 24);
            modeLabel.setTopLeftAnchor(0, 0);
            modeLabel.setBottomRightAnchor(1, 0);
            modeLabel.setTextColor(UIColor.matWhite());
        });
        bottomPanel.addChildComponent("modeLabel", modeLabel);
        //</editor-fold>

        //<editor-fold desc="ProjectManager.getCurrentProject().mode Direction Label">
        //Defined at class level
        modeDirectionLabel.setOnInitAction(() -> {
            modeDirectionLabel.setTopLeftPos(4, 24);
            modeDirectionLabel.setBottomRightPos(-4, 48);
            modeDirectionLabel.setTopLeftAnchor(0, 0);
            modeDirectionLabel.setBottomRightAnchor(1, 0);
        });
        bottomPanel.addChildComponent("modeDirectionLabel", modeDirectionLabel);
        //</editor-fold>

        final Panel leftPanel = new Panel();
        leftPanel.setOnInitAction(() -> {
            leftPanel.setBackgroundColor(UIColor.matGrey900(0.75));
            leftPanel.setTopLeftPos(0, 0);
            leftPanel.setBottomRightPos(256, 0 - TIMELINE_HEIGHT);
            leftPanel.setTopLeftAnchor(0, 0);
            leftPanel.setBottomRightAnchor(0, 1);
        });
        mainUI.addChildComponent("leftPanel", leftPanel);

        //<editor-fold desc="Placeables / Objects mode buttons">
        final TextButton outlinerPlaceablesTabButton = new TextButton();
        final TextButton outlinerObjectsTabButton = new TextButton();

        //Defined above
        outlinerPlaceablesTabButton.setOnInitAction(() -> {
            outlinerPlaceablesTabButton.setTopLeftPos(0, 0);
            outlinerPlaceablesTabButton.setBottomRightPos(-1, 24);
            outlinerPlaceablesTabButton.setTopLeftAnchor(0, 0);
            outlinerPlaceablesTabButton.setBottomRightAnchor(0.5, 0);
            outlinerPlaceablesTabButton.setText(LangManager.getItem("placeables"));
            outlinerPlaceablesTabButton.setBackgroundIdleColor(UIColor.matBlue900());
        });
        outlinerPlaceablesTabButton.setOnLMBAction(() -> {
            outlinerPlaceablesTabButton.setBackgroundIdleColor(UIColor.matBlue900());
            outlinerObjectsTabButton.setBackgroundIdleColor(UIColor.matBlue());

            addPlaceablePanel.setVisible(true);
            outlinerPlaceablesPanel.setVisible(true);
            outlinerObjectsPanel.setVisible(false);

            propertiesPlaceablesListBox.setVisible(true);
            propertiesObjectsListBox.setVisible(false);

            if (ProjectManager.getCurrentProject() != null && ProjectManager.getCurrentClientLevelData() != null) {
                ProjectManager.getCurrentClientLevelData().clearSelectedObjects();
            }

            objectMode = EnumObjectMode.PLACEABLE_EDIT;
        });
        leftPanel.addChildComponent("outlinerPlaceablesTabButton", outlinerPlaceablesTabButton);

        //Defined above
        outlinerObjectsTabButton.setOnInitAction(() -> {
            outlinerObjectsTabButton.setTopLeftPos(1, 0);
            outlinerObjectsTabButton.setBottomRightPos(0, 24);
            outlinerObjectsTabButton.setTopLeftAnchor(0.5, 0);
            outlinerObjectsTabButton.setBottomRightAnchor(1, 0);
            outlinerObjectsTabButton.setText(LangManager.getItem("objects"));
            outlinerObjectsTabButton.setBackgroundIdleColor(UIColor.matBlue());
        });
        outlinerObjectsTabButton.setOnLMBAction(() -> {
            outlinerPlaceablesTabButton.setBackgroundIdleColor(UIColor.matBlue());
            outlinerObjectsTabButton.setBackgroundIdleColor(UIColor.matBlue900());

            addPlaceablePanel.setVisible(false);
            outlinerPlaceablesPanel.setVisible(false);
            outlinerObjectsPanel.setVisible(true);

            propertiesPlaceablesListBox.setVisible(false);
            propertiesObjectsListBox.setVisible(true);

            if (ProjectManager.getCurrentProject() != null && ProjectManager.getCurrentClientLevelData() != null) {
                ProjectManager.getCurrentClientLevelData().clearSelectedPlaceables();
            }

            objectMode = EnumObjectMode.OBJECT_EDIT;
        });
        leftPanel.addChildComponent("outlinerObjectsTabButton", outlinerObjectsTabButton);
        //</editor-fold>

        //Defined at class level
        addPlaceablePanel.setOnInitAction(() -> {
            addPlaceablePanel.setTopLeftPos(0, 24);
            addPlaceablePanel.setBottomRightPos(0, 168);
            addPlaceablePanel.setTopLeftAnchor(0, 0);
            addPlaceablePanel.setBottomRightAnchor(1, 0);
            addPlaceablePanel.setBackgroundColor(UIColor.transparent());
        });
        leftPanel.addChildComponent("addPlaceablePanel", addPlaceablePanel);

        final Label addPlaceableLabel = new Label();
        addPlaceableLabel.setOnInitAction(() -> {
            addPlaceableLabel.setText(LangManager.getItem("addPlaceable"));
            addPlaceableLabel.setHorizontalAlign(EnumHAlignment.centre);
            addPlaceableLabel.setVerticalAlign(EnumVAlignment.centre);
            addPlaceableLabel.setTopLeftPos(4, 0);
            addPlaceableLabel.setBottomRightPos(-4, 24);
            addPlaceableLabel.setTopLeftAnchor(0, 0);
            addPlaceableLabel.setBottomRightAnchor(1, 0);
        });
        addPlaceablePanel.addChildComponent("addPlaceableLabel", addPlaceableLabel);

        final ListBox addPlaceableListBox = new ListBox();
        addPlaceableListBox.setOnInitAction(() -> {
            addPlaceableListBox.setBackgroundColor(UIColor.transparent());
            addPlaceableListBox.setTopLeftPos(0, 24);
            addPlaceableListBox.setBottomRightPos(0, 0);
            addPlaceableListBox.setTopLeftAnchor(0, 0);
            addPlaceableListBox.setBottomRightAnchor(1, 1);
            addPlaceableListBox.setCanScroll(false);
        });
        addPlaceablePanel.addChildComponent("addPlaceableListBox", addPlaceableListBox);

        //<editor-fold desc="Add placeable buttons">
        for (IAsset asset : AssetManager.getAvaliableAssets()) {
            final TextButton placeableButton = new TextButton();
            placeableButton.setOnInitAction(() -> {
                placeableButton.setText(LangManager.getItem(asset.getName()));
                placeableButton.setTopLeftPos(0, 0);
                placeableButton.setBottomRightPos(0, 18);
            });
            placeableButton.setOnLMBAction(() -> addPlaceable(new Placeable(asset.getCopy())));
            addPlaceableListBox.addChildComponent(asset.getName() + "AddPlaceableButton", placeableButton);
        }
        //</editor-fold>

        //<editor-fold desc="Outliner placeables panel">
        //Defined at class level
        outlinerPlaceablesPanel.setOnInitAction(() -> {
            outlinerPlaceablesPanel.setTopLeftPos(0, 168);
            outlinerPlaceablesPanel.setBottomRightPos(0, 0);
            outlinerPlaceablesPanel.setTopLeftAnchor(0, 0);
            outlinerPlaceablesPanel.setBottomRightAnchor(1, 1);
            outlinerPlaceablesPanel.setBackgroundColor(UIColor.transparent());
        });
        leftPanel.addChildComponent("outlinerPlaceablesPanel", outlinerPlaceablesPanel);

        final Label outlinerPlaceablesLabel = new Label();
        outlinerPlaceablesLabel.setOnInitAction(() -> {
            outlinerPlaceablesLabel.setText(LangManager.getItem("outliner"));
            outlinerPlaceablesLabel.setHorizontalAlign(EnumHAlignment.centre);
            outlinerPlaceablesLabel.setVerticalAlign(EnumVAlignment.centre);
            outlinerPlaceablesLabel.setTopLeftPos(4, 0);
            outlinerPlaceablesLabel.setBottomRightPos(-4, 24);
            outlinerPlaceablesLabel.setTopLeftAnchor(0, 0);
            outlinerPlaceablesLabel.setBottomRightAnchor(1, 0);
        });
        outlinerPlaceablesPanel.addChildComponent("outlinerPlaceablesLabel", outlinerPlaceablesLabel);

        //Defined at class level
        outlinerPlaceablesListBox.setOnInitAction(() -> {
            outlinerPlaceablesListBox.setBackgroundColor(UIColor.transparent());
            outlinerPlaceablesListBox.setTopLeftPos(0, 24);
            outlinerPlaceablesListBox.setBottomRightPos(0, 0);
            outlinerPlaceablesListBox.setTopLeftAnchor(0, 0);
            outlinerPlaceablesListBox.setBottomRightAnchor(1, 1);
        });
        outlinerPlaceablesPanel.addChildComponent("outlinerPlaceablesListBox", outlinerPlaceablesListBox);
        //</editor-fold>

        //<editor-fold desc="Outliner objects panel">
        //Defined at class level
        outlinerObjectsPanel.setOnInitAction(() -> {
            outlinerObjectsPanel.setTopLeftPos(0, 24);
            outlinerObjectsPanel.setBottomRightPos(0, 0);
            outlinerObjectsPanel.setTopLeftAnchor(0, 0);
            outlinerObjectsPanel.setBottomRightAnchor(1, 1);
            outlinerObjectsPanel.setBackgroundColor(UIColor.transparent());
            outlinerObjectsPanel.setVisible(false); //Hidden by default
        });
        leftPanel.addChildComponent("outlinerObjectsPanel", outlinerObjectsPanel);

        final Label outlinerObjectsLabel = new Label();
        outlinerObjectsLabel.setOnInitAction(() -> {
            outlinerObjectsLabel.setText(LangManager.getItem("outliner"));
            outlinerObjectsLabel.setHorizontalAlign(EnumHAlignment.centre);
            outlinerObjectsLabel.setVerticalAlign(EnumVAlignment.centre);
            outlinerObjectsLabel.setTopLeftPos(4, 0);
            outlinerObjectsLabel.setBottomRightPos(-4, 24);
            outlinerObjectsLabel.setTopLeftAnchor(0, 0);
            outlinerObjectsLabel.setBottomRightAnchor(1, 0);
        });
        outlinerObjectsPanel.addChildComponent("outlinerObjectsLabel", outlinerObjectsLabel);

        //Defined at class level
        outlinerObjectsListBox.setOnInitAction(() -> {
            outlinerObjectsListBox.setBackgroundColor(UIColor.transparent());
            outlinerObjectsListBox.setTopLeftPos(0, 24);
//            outlinerObjectsListBox.setBottomRightPos(0, -24); //TODO: When external backgrounds are figured out, uncomment this
            outlinerObjectsListBox.setBottomRightPos(0, 0);
            outlinerObjectsListBox.setTopLeftAnchor(0, 0);
            outlinerObjectsListBox.setBottomRightAnchor(1, 1);
        });
        outlinerObjectsPanel.addChildComponent("outlinerObjectsListBox", outlinerObjectsListBox);
        //</editor-fold>

//        final TextField addExternalBackgroundObjectTextField = new TextField();
//        addExternalBackgroundObjectTextField.setOnInitAction(() -> {
//            addExternalBackgroundObjectTextField.setTopLeftPos(0, -24);
//            addExternalBackgroundObjectTextField.setBottomRightPos(0, 0);
//            addExternalBackgroundObjectTextField.setTopLeftAnchor(0, 1);
//            addExternalBackgroundObjectTextField.setBottomRightAnchor(1, 1);
//            addExternalBackgroundObjectTextField.setPlaceholder(LangManager.getItem("addExternalBackgroundObject"));
//            addExternalBackgroundObjectTextField.setBackgroundColor(UIColor.transparent());
//        });
//        addExternalBackgroundObjectTextField.setOnReturnAction(() -> {
//            if (ProjectManager.getCurrentProject() != null && ProjectManager.getCurrentClientLevelData() != null) {
//                if (!Objects.equals(addExternalBackgroundObjectTextField.value, "")) {
//                    if (!ProjectManager.getCurrentLevelData().isObjectBackgroundExternal(addExternalBackgroundObjectTextField.text) &&
//                            !ProjectManager.getCurrentLevelData().getModel().hasObject(addExternalBackgroundObjectTextField.text)) {
//                        ProjectManager.getCurrentLevelData().addBackgroundExternalObject(addExternalBackgroundObjectTextField.text);
//
//                        outlinerObjectsListBox.addChildComponent(getOutlinerExternalBackgroundObjectComponent(addExternalBackgroundObjectTextField.text));
//                    } else {
//                        sendNotif(LangManager.getItem("alreadyObject"), UIColor.matRed());
//                    }
//
//                    addExternalBackgroundObjectTextField.setValue(""); //Clear text field
//                } else {
//                    //Text field is blank
//                    sendNotif(LangManager.getItem("noObjectSpecified"), UIColor.matRed());
//                }
//            } else {
//                sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
//            }
//        });
//        outlinerObjectsPanel.addChildComponent("addExternalBackgroundObjectTextField", addExternalBackgroundObjectTextField);
//        textFields.add(addExternalBackgroundObjectTextField);

        final Panel rightPanel = new Panel();
        rightPanel.setOnInitAction(() -> {
            rightPanel.setBackgroundColor(UIColor.matGrey900(0.75));
            rightPanel.setTopLeftPos(-256, 0);
            rightPanel.setBottomRightPos(0, 0 - TIMELINE_HEIGHT);
            rightPanel.setTopLeftAnchor(1, 0);
            rightPanel.setBottomRightAnchor(1, 1);
        });
        mainUI.addChildComponent("rightPanel", rightPanel);

        final ListBox actionsListBox = new ListBox();
        actionsListBox.setOnInitAction(() -> {
            actionsListBox.setTopLeftPos(0, 0);
            actionsListBox.setBottomRightPos(0, 130);
            actionsListBox.setTopLeftAnchor(0, 0);
            actionsListBox.setBottomRightAnchor(1, 0);
            actionsListBox.setBackgroundColor(UIColor.transparent());
        });
        rightPanel.addChildComponent("actionsListBox", actionsListBox);

        //<editor-fold desc="ImportObj TextButton">
        //Defined at class level
        importObjButton.setOnInitAction(() -> {
            importObjButton.setText(LangManager.getItem("importObj"));
            importObjButton.setTopLeftPos(0, 0);
            importObjButton.setBottomRightPos(0, 24);
        });
        importObjButton.setOnLMBAction(this::importObj);
        actionsListBox.addChildComponent("importObjButton", importObjButton);
        //</editor-fold>

        //<editor-fold desc="ImportConfig TextButton">
        //Defined at class level
        importConfigButton.setOnInitAction(() -> {
            importConfigButton.setText(LangManager.getItem("importConfig"));
            importConfigButton.setTopLeftPos(0, 0);
            importConfigButton.setBottomRightPos(0, 24);
        });
        importConfigButton.setOnLMBAction(this::importConfig);
        actionsListBox.addChildComponent("importConfigButton", importConfigButton);
        //</editor-fold>

        //<editor-fold desc="Export TextButton">
        //Defined at class level
        exportButton.setOnInitAction(() -> {
            exportButton.setText(LangManager.getItem("export"));
            exportButton.setTopLeftPos(0, 0);
            exportButton.setBottomRightPos(0, 24);
        });
        exportButton.setOnLMBAction(this::export);
        actionsListBox.addChildComponent("exportButton", exportButton);
        //</editor-fold>

        //<editor-fold desc="Settings TextButton">
        //Defined at class level
        settingsButton.setOnInitAction(() -> {
            settingsButton.setText(LangManager.getItem("settings"));
            settingsButton.setTopLeftPos(0, 0);
            settingsButton.setBottomRightPos(0, 24);
        });
        settingsButton.setOnLMBAction(this::showSettings);
        actionsListBox.addChildComponent("settingsButton", settingsButton);
        //</editor-fold>

        //<editor-fold desc="Project Settings TextButton">
        //Defined at class level
        projectSettingsButton.setOnInitAction(() -> {
            projectSettingsButton.setText(LangManager.getItem("projectSettings"));
            projectSettingsButton.setTopLeftPos(0, 0);
            projectSettingsButton.setBottomRightPos(0, 24);
        });
        projectSettingsButton.setOnLMBAction(this::showProjectSettings);
        actionsListBox.addChildComponent("projectSettingsButton", projectSettingsButton);
        //</editor-fold>

        //<editor-fold desc="Placeable Properties">
        //Defined at class level
        propertiesPlaceablesListBox.setOnInitAction(() -> {
            propertiesPlaceablesListBox.setTopLeftPos(0, 130);
            propertiesPlaceablesListBox.setBottomRightPos(0, 0);
            propertiesPlaceablesListBox.setTopLeftAnchor(0, 0);
            propertiesPlaceablesListBox.setBottomRightAnchor(1, 1);
            propertiesPlaceablesListBox.setBackgroundColor(UIColor.transparent());
        });
        rightPanel.addChildComponent("propertiesPlaceablesListBox", propertiesPlaceablesListBox);

        final Label placeablesPropertiesLabel = new Label();
        placeablesPropertiesLabel.setOnInitAction(() -> {
            placeablesPropertiesLabel.setText(LangManager.getItem("properties"));
            placeablesPropertiesLabel.setHorizontalAlign(EnumHAlignment.centre);
            placeablesPropertiesLabel.setVerticalAlign(EnumVAlignment.centre);
            placeablesPropertiesLabel.setTopLeftPos(0, 0);
            placeablesPropertiesLabel.setBottomRightPos(0, 28);
            placeablesPropertiesLabel.setTopLeftAnchor(0, 0);
            placeablesPropertiesLabel.setBottomRightAnchor(1, 0);
        });
        propertiesPlaceablesListBox.addChildComponent("placeablesPropertiesLabel", placeablesPropertiesLabel);

        final Label placeablePositionLabel = new Label();
        placeablePositionLabel.setOnInitAction(() -> {
            placeablePositionLabel.setText(LangManager.getItem("position"));
            placeablePositionLabel.setVerticalAlign(EnumVAlignment.centre);
            placeablePositionLabel.setTopLeftPos(0, 0);
            placeablePositionLabel.setBottomRightPos(0, 24);
        });
        propertiesPlaceablesListBox.addChildComponent("placeablePositionLabel", placeablePositionLabel);

        //Defined at class level
        placeablePositionTextFields.setOnInitAction(() -> {
            placeablePositionTextFields.setTopLeftPos(0, 0);
            placeablePositionTextFields.setBottomRightPos(0, 76);
        });
        propertiesPlaceablesListBox.addChildComponent("placeablePositionTextFields", placeablePositionTextFields);

        final Label rotationLabel = new Label();
        rotationLabel.setOnInitAction(() -> {
            rotationLabel.setText(LangManager.getItem("rotation"));
            rotationLabel.setVerticalAlign(EnumVAlignment.centre);
            rotationLabel.setTopLeftPos(0, 0);
            rotationLabel.setBottomRightPos(0, 24);
        });
        propertiesPlaceablesListBox.addChildComponent("rotationLabel", rotationLabel);

        //Defined at class level
        placeableRotationTextFields.setOnInitAction(() -> {
            placeableRotationTextFields.setTopLeftPos(0, 0);
            placeableRotationTextFields.setBottomRightPos(0, 76);
        });
        propertiesPlaceablesListBox.addChildComponent("placeableRotationTextFields", placeableRotationTextFields);

        final Label scaleLabel = new Label();
        scaleLabel.setOnInitAction(() -> {
            scaleLabel.setText(LangManager.getItem("scale"));
            scaleLabel.setVerticalAlign(EnumVAlignment.centre);
            scaleLabel.setTopLeftPos(0, 0);
            scaleLabel.setBottomRightPos(0, 24);
        });
        propertiesPlaceablesListBox.addChildComponent("scaleLabel", scaleLabel);

        //Defined at class level
        placeableScaleTextFields.setOnInitAction(() -> {
            placeableScaleTextFields.setTopLeftPos(0, 0);
            placeableScaleTextFields.setBottomRightPos(0, 76);
        });
        propertiesPlaceablesListBox.addChildComponent("placeableScaleTextFields", placeableScaleTextFields);

        final Label typeLabel = new Label();
        typeLabel.setOnInitAction(() -> {
            typeLabel.setText(LangManager.getItem("type"));
            typeLabel.setVerticalAlign(EnumVAlignment.centre);
            typeLabel.setTopLeftPos(0, 0);
            typeLabel.setBottomRightPos(0, 24);
        });
        propertiesPlaceablesListBox.addChildComponent("typeLabel", typeLabel);

        //Defined at class level
        typeButton.setOnInitAction(() -> {
            typeButton.setText(LangManager.getItem("noTypes"));
            typeButton.setEnabled(false);
            typeButton.setTopLeftPos(0, 0);
            typeButton.setBottomRightPos(0, 24);
        });
        typeButton.setOnLMBAction(() -> {
            assert mousePos != null;
            setOverlayUiScreen(getTypeSelectorOverlayScreen(mousePos.y));
        });
        propertiesPlaceablesListBox.addChildComponent("typeButton", typeButton);

        final Label placeableItemGroupLabel = new Label();
        placeableItemGroupLabel.setOnInitAction(() -> {
            placeableItemGroupLabel.setText(LangManager.getItem("itemGroup"));
            placeableItemGroupLabel.setVerticalAlign(EnumVAlignment.centre);
            placeableItemGroupLabel.setTopLeftPos(0, 0);
            placeableItemGroupLabel.setBottomRightPos(0, 24);
        });
        propertiesPlaceablesListBox.addChildComponent("itemGroupLabel", placeableItemGroupLabel);

        //Defined at class level
        placeableItemGroupButton.setOnInitAction(() -> {
            placeableItemGroupButton.setText(LangManager.getItem("nothingSelected"));
            placeableItemGroupButton.setEnabled(false);
            placeableItemGroupButton.setTopLeftPos(0, 0);
            placeableItemGroupButton.setBottomRightPos(0, 24);
        });
        placeableItemGroupButton.setOnLMBAction(() -> {
            assert mousePos != null;
            setOverlayUiScreen(getItemGroupSelectorOverlayScreen(mousePos.x, mousePos.y));
        });
        propertiesPlaceablesListBox.addChildComponent("placeableItemGroupButton", placeableItemGroupButton);
        //</editor-fold>

        //<editor-fold desc="Object Properties">
        //Defined at class level
        propertiesObjectsListBox.setOnInitAction(() -> {
            propertiesObjectsListBox.setTopLeftPos(0, 130);
            propertiesObjectsListBox.setBottomRightPos(0, 0);
            propertiesObjectsListBox.setTopLeftAnchor(0, 0);
            propertiesObjectsListBox.setBottomRightAnchor(1, 1);
            propertiesObjectsListBox.setBackgroundColor(UIColor.transparent());
            propertiesObjectsListBox.setVisible(false);
        });
        rightPanel.addChildComponent("propertiesObjectsListBox", propertiesObjectsListBox);

        final Label objectsPropertiesLabel = new Label();
        objectsPropertiesLabel.setOnInitAction(() -> {
            objectsPropertiesLabel.setText(LangManager.getItem("properties"));
            objectsPropertiesLabel.setHorizontalAlign(EnumHAlignment.centre);
            objectsPropertiesLabel.setVerticalAlign(EnumVAlignment.centre);
            objectsPropertiesLabel.setTopLeftPos(0, 0);
            objectsPropertiesLabel.setBottomRightPos(0, 28);
            objectsPropertiesLabel.setTopLeftAnchor(0, 0);
            objectsPropertiesLabel.setBottomRightAnchor(1, 0);
        });
        propertiesObjectsListBox.addChildComponent("objectsPropertiesLabel", objectsPropertiesLabel);

        final Label objectItemGroupLabel = new Label();
        objectItemGroupLabel.setOnInitAction(() -> {
            objectItemGroupLabel.setText(LangManager.getItem("itemGroup"));
            objectItemGroupLabel.setVerticalAlign(EnumVAlignment.centre);
            objectItemGroupLabel.setTopLeftPos(0, 0);
            objectItemGroupLabel.setBottomRightPos(0, 24);
        });
        propertiesObjectsListBox.addChildComponent("objectItemGroupLabel", objectItemGroupLabel);

        //Defined at class level
        objectItemGroupButton.setOnInitAction(() -> {
            objectItemGroupButton.setText(LangManager.getItem("nothingSelected"));
            objectItemGroupButton.setEnabled(false);
            objectItemGroupButton.setTopLeftPos(0, 0);
            objectItemGroupButton.setBottomRightPos(0, 24);
        });
        objectItemGroupButton.setOnLMBAction(() -> {
            assert mousePos != null;
            setOverlayUiScreen(getItemGroupSelectorOverlayScreen(mousePos.x, mousePos.y));
        });
        propertiesObjectsListBox.addChildComponent("objectItemGroupButton", objectItemGroupButton);
        //</editor-fold>

        //Defined at class level
        notifPanel.setOnInitAction(() -> {
            notifPanel.setTopLeftPos(0, 0);
            notifPanel.setBottomRightPos(0, 0);
            notifPanel.setTopLeftAnchor(0, 0);
            notifPanel.setBottomRightAnchor(1, 1);
            notifPanel.setBackgroundColor(UIColor.transparent());
        });
        mainUI.addChildComponent("notifPanel", notifPanel);

        //Defined at class level
        timeline.setOnInitAction(() -> {
            timeline.setTopLeftPos(0, -TIMELINE_HEIGHT);
            timeline.setBottomRightPos(0, 0);
            timeline.setTopLeftAnchor(0, 1);
            timeline.setBottomRightAnchor(1, 1);
            timeline.setBackgroundColor(UIColor.matGrey900(0.75));
        });
        mainUI.addChildComponent("timeline", timeline);

        //<editor-fold desc="On screen camera controls">
        //On screen camera controls - For those with difficulty controlling the camera with MMB / WASDQE
        //Declared at class level
        onScreenCameraControlsPanel.setOnInitAction(() -> {
            onScreenCameraControlsPanel.setBackgroundColor(UIColor.matGrey900(0.75));
            onScreenCameraControlsPanel.setTopLeftAnchor(0.5, 1);
            onScreenCameraControlsPanel.setBottomRightAnchor(0.5, 1);
            onScreenCameraControlsPanel.setTopLeftPos(-36, -72 -TIMELINE_HEIGHT - 52);
            onScreenCameraControlsPanel.setBottomRightPos(36, -TIMELINE_HEIGHT - 52);
            onScreenCameraControlsPanel.setVisible(false);
        });
        mainUI.addChildComponent("onScreenCameraControlsPanel", onScreenCameraControlsPanel);

        final Button cameraPanDownButton = new Button();
        cameraPanDownButton.setOnInitAction(() -> {
            cameraPanDownButton.setTopLeftAnchor(0, 0);
            cameraPanDownButton.setBottomRightAnchor(0, 0);
            cameraPanDownButton.setTopLeftPos(0, 0);
            cameraPanDownButton.setBottomRightPos(24, 24);
            cameraPanDownButton.setTexture(ResourceManager.getTexture("image/arrowRightDown").getTexture());
        });
        cameraPanDownButton.addPlugin(new AbstractComponentPlugin() {
            @Override
            public void onPostDraw() {
                if (linkedComponent.mouseOver && Mouse.isButtonDown(0)) {
                    double speed = Window.isShiftDown() ? SMBLWSettings.cameraSprintSpeedMultiplier * SMBLWSettings.cameraSpeed : SMBLWSettings.cameraSpeed;
                    cameraPos = cameraPos.add(0, -UIUtils.getDelta() * speed, 0);
                }
            }
        });
        onScreenCameraControlsPanel.addChildComponent("cameraPanDownButton", cameraPanDownButton);

        final Button cameraPanLeftButton = new Button();
        cameraPanLeftButton.setOnInitAction(() -> {
            cameraPanLeftButton.setTopLeftAnchor(0, 0);
            cameraPanLeftButton.setBottomRightAnchor(0, 0);
            cameraPanLeftButton.setTopLeftPos(0, 24);
            cameraPanLeftButton.setBottomRightPos(24, 48);
            cameraPanLeftButton.setTexture(ResourceManager.getTexture("image/arrowLeft").getTexture());
        });
        cameraPanLeftButton.addPlugin(new AbstractComponentPlugin() {
            @Override
            public void onPostDraw() {
                if (linkedComponent.mouseOver && Mouse.isButtonDown(0)) {
                    double speed = Window.isShiftDown() ? SMBLWSettings.cameraSprintSpeedMultiplier * SMBLWSettings.cameraSpeed : SMBLWSettings.cameraSpeed;
                    PosXYZ rightVector = new PosXYZ(Math.sin(Math.toRadians(cameraRot.x + 90)), 0, -Math.cos(Math.toRadians(cameraRot.x + 90)));
                    cameraPos = cameraPos.subtract(rightVector.multiply(UIUtils.getDelta()).multiply(speed));
                }
            }
        });
        onScreenCameraControlsPanel.addChildComponent("cameraPanLeftButton", cameraPanLeftButton);

        final Button cameraPanForwardButton = new Button();
        cameraPanForwardButton.setOnInitAction(() -> {
            cameraPanForwardButton.setTopLeftAnchor(0, 0);
            cameraPanForwardButton.setBottomRightAnchor(0, 0);
            cameraPanForwardButton.setTopLeftPos(24, 0);
            cameraPanForwardButton.setBottomRightPos(48, 24);
            cameraPanForwardButton.setTexture(ResourceManager.getTexture("image/arrowUp").getTexture());
        });
        cameraPanForwardButton.addPlugin(new AbstractComponentPlugin() {
            @Override
            public void onPostDraw() {
                if (linkedComponent.mouseOver && Mouse.isButtonDown(0)) {
                    double speed = Window.isShiftDown() ? SMBLWSettings.cameraSprintSpeedMultiplier * SMBLWSettings.cameraSpeed : SMBLWSettings.cameraSpeed;
                    PosXYZ forwardVector = new PosXYZ(Math.sin(Math.toRadians(cameraRot.x)), 0, -Math.cos(Math.toRadians(cameraRot.x)));
                    cameraPos = cameraPos.add(forwardVector.multiply(UIUtils.getDelta()).multiply(speed));
                }
            }
        });
        onScreenCameraControlsPanel.addChildComponent("cameraPanForwardButton", cameraPanForwardButton);

        final Image cameraPanIcon = new Image();
        cameraPanIcon.setOnInitAction(() -> {
            cameraPanIcon.setTexture(ResourceManager.getTexture("image/arrowAll").getTexture());
            cameraPanIcon.setTopLeftAnchor(0, 0);
            cameraPanIcon.setBottomRightAnchor(0, 0);
            cameraPanIcon.setTopLeftPos(24, 24);
            cameraPanIcon.setBottomRightPos(48, 48);
        });
        onScreenCameraControlsPanel.addChildComponent("cameraPanIcon", cameraPanIcon);

        final Button cameraPanBackwardsButton = new Button();
        cameraPanBackwardsButton.setOnInitAction(() -> {
            cameraPanBackwardsButton.setTopLeftAnchor(0, 0);
            cameraPanBackwardsButton.setBottomRightAnchor(0, 0);
            cameraPanBackwardsButton.setTopLeftPos(24, 48);
            cameraPanBackwardsButton.setBottomRightPos(48, 72);
            cameraPanBackwardsButton.setTexture(ResourceManager.getTexture("image/arrowDown").getTexture());
        });
        cameraPanBackwardsButton.addPlugin(new AbstractComponentPlugin() {
            @Override
            public void onPostDraw() {
                if (linkedComponent.mouseOver && Mouse.isButtonDown(0)) {
                    double speed = Window.isShiftDown() ? SMBLWSettings.cameraSprintSpeedMultiplier * SMBLWSettings.cameraSpeed : SMBLWSettings.cameraSpeed;
                    PosXYZ forwardVector = new PosXYZ(Math.sin(Math.toRadians(cameraRot.x)), 0, -Math.cos(Math.toRadians(cameraRot.x)));
                    cameraPos = cameraPos.subtract(forwardVector.multiply(UIUtils.getDelta()).multiply(speed));
                }
            }
        });
        onScreenCameraControlsPanel.addChildComponent("cameraPanBackwardsButton", cameraPanBackwardsButton);

        final Button cameraPanUpButton = new Button();
        cameraPanUpButton.setOnInitAction(() -> {
            cameraPanUpButton.setTopLeftAnchor(0, 0);
            cameraPanUpButton.setBottomRightAnchor(0, 0);
            cameraPanUpButton.setTopLeftPos(48, 0);
            cameraPanUpButton.setBottomRightPos(72, 24);
            cameraPanUpButton.setTexture(ResourceManager.getTexture("image/arrowLeftUp").getTexture());
        });
        cameraPanUpButton.addPlugin(new AbstractComponentPlugin() {
            @Override
            public void onPostDraw() {
                if (linkedComponent.mouseOver && Mouse.isButtonDown(0)) {
                    double speed = Window.isShiftDown() ? SMBLWSettings.cameraSprintSpeedMultiplier * SMBLWSettings.cameraSpeed : SMBLWSettings.cameraSpeed;
                    cameraPos = cameraPos.add(0, UIUtils.getDelta() * speed, 0);
                }
            }
        });
        onScreenCameraControlsPanel.addChildComponent("cameraPanUpButton", cameraPanUpButton);

        final Button cameraPanRightButton = new Button();
        cameraPanRightButton.setOnInitAction(() -> {
            cameraPanRightButton.setTopLeftAnchor(0, 0);
            cameraPanRightButton.setBottomRightAnchor(0, 0);
            cameraPanRightButton.setTopLeftPos(48, 24);
            cameraPanRightButton.setBottomRightPos(72, 48);
            cameraPanRightButton.setTexture(ResourceManager.getTexture("image/arrowRight").getTexture());
        });
        cameraPanRightButton.addPlugin(new AbstractComponentPlugin() {
            @Override
            public void onPostDraw() {
                if (linkedComponent.mouseOver && Mouse.isButtonDown(0)) {
                    double speed = Window.isShiftDown() ? SMBLWSettings.cameraSprintSpeedMultiplier * SMBLWSettings.cameraSpeed : SMBLWSettings.cameraSpeed;
                    PosXYZ rightVector = new PosXYZ(Math.sin(Math.toRadians(cameraRot.x + 90)), 0, -Math.cos(Math.toRadians(cameraRot.x + 90)));
                    cameraPos = cameraPos.add(rightVector.multiply(UIUtils.getDelta()).multiply(speed));
                }
            }
        });
        onScreenCameraControlsPanel.addChildComponent("cameraPanRightButton", cameraPanRightButton);
        //</editor-fold>

        //Defined at class level
        inputOverlay.setOnInitAction(() -> {
            inputOverlay.setTopLeftPos(256, 0);
            inputOverlay.setBottomRightPos(-256, -TIMELINE_HEIGHT);
            inputOverlay.setTopLeftAnchor(0, 0);
            inputOverlay.setBottomRightAnchor(1, 1);
            inputOverlay.setBackgroundColor(UIColor.transparent());
        });
        mainUI.addChildComponent("inputOverlay", inputOverlay);

        //Defined at class level
        fpsOverlay.setOnInitAction(() -> {
            fpsOverlay.setTopLeftPos(260, 4);
            fpsOverlay.setBottomRightPos(516, 28);
            fpsOverlay.setTopLeftAnchor(0, 0);
            fpsOverlay.setBottomRightAnchor(0, 0);
            fpsOverlay.setBackgroundColor(UIColor.matGrey900(0.75));
            fpsOverlay.setVisible(false);
        });
        mainUI.addChildComponent("fpsOverlay", fpsOverlay);

        Window.logOpenGLError("After MainScreen.init()");

        try {
            Window.drawable.releaseContext();
        } catch (LWJGLException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void preDraw() {
        super.preDraw();

        Window.logOpenGLError("After MainScreen.super.preDraw()");

        //Run next frame actions
        nextFrameActions.forEach(UIAction::execute);
        nextFrameActions.clear();

        //Show on screen camera controls if setting enabled
        onScreenCameraControlsPanel.setVisible(SMBLWSettings.showOnScreenCameraControls);
        //Show on screen input if setting enabled
        inputOverlay.setVisible(SMBLWSettings.showOnScreenInput);
        //Show FPS if setting enabled
        fpsOverlay.setVisible(SMBLWSettings.showFPSOverlay);

        if (ProjectManager.getCurrentClientLevelData() != null) {
            ProjectManager.getCurrentClientLevelData().update((float) UIUtils.getDelta());
        }

        if (Mouse.isButtonDown(2)) { //If MMB down
            //<editor-fold desc="Rotate camera on MMB & Move camera with MMB & WASDQE">
            cameraRot = cameraRot.add(UIUtils.getMouseDelta().toPosXY());

            //X
            if (cameraRot.x >= 360) {
                cameraRot.x -= 360;
            } else if (cameraRot.x <= -360) {
                cameraRot.x += 360;
            }

            //Y
            if (cameraRot.y > 90) {
                cameraRot.y = 90;
            } else if (cameraRot.y < -90) {
                cameraRot.y = -90;
            }

            PosXYZ forwardVector = new PosXYZ(Math.sin(Math.toRadians(cameraRot.x)), 0, -Math.cos(Math.toRadians(cameraRot.x)));
            PosXYZ rightVector = new PosXYZ(Math.sin(Math.toRadians(cameraRot.x + 90)), 0, -Math.cos(Math.toRadians(cameraRot.x + 90)));

            double speed = Window.isShiftDown() ? SMBLWSettings.cameraSprintSpeedMultiplier * SMBLWSettings.cameraSpeed : SMBLWSettings.cameraSpeed;

            if (Keyboard.isKeyDown(Keyboard.KEY_Q)) { //Q: Go Down
                cameraPos = cameraPos.add(0, -UIUtils.getDelta() * speed, 0);
            }

            if (Keyboard.isKeyDown(Keyboard.KEY_E)) { //E: Go Up
                cameraPos = cameraPos.add(0, UIUtils.getDelta() * speed, 0);
            }

            if (Keyboard.isKeyDown(Keyboard.KEY_W)) { //W: Go Forwards
                cameraPos = cameraPos.add(forwardVector.multiply(UIUtils.getDelta()).multiply(speed));
            }

            if (Keyboard.isKeyDown(Keyboard.KEY_S)) { //S: Go Backwards
                cameraPos = cameraPos.subtract(forwardVector.multiply(UIUtils.getDelta()).multiply(speed));
            }

            if (Keyboard.isKeyDown(Keyboard.KEY_D)) { //D: Go Right
                cameraPos = cameraPos.add(rightVector.multiply(UIUtils.getDelta()).multiply(speed));
            }

            if (Keyboard.isKeyDown(Keyboard.KEY_A)) { //A: Go Left
                cameraPos = cameraPos.subtract(rightVector.multiply(UIUtils.getDelta()).multiply(speed));
            }
            //</editor-fold>
        } else if (ProjectManager.getCurrentClientLevelData() != null) {
            if (ProjectManager.getCurrentProject().mode == EnumActionMode.GRAB_PLACEABLE) {
                //<editor-fold desc="Grab">

                for (String key : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
                    Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(key);

                    if ((ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(1, 0, 0)) && placeable.getAsset().canGrabX()) ||
                            (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(0, 1, 0)) && placeable.getAsset().canGrabY()) ||
                            (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(0, 0, 1)) && placeable.getAsset().canGrabZ()) ||
                            (placeable.getAsset().canGrabX() && placeable.getAsset().canGrabY()) && placeable.getAsset().canGrabZ()) { //If can grab in selected direction
                        if (Window.isAltDown()) { //Snap with Alt
                            if (Window.isShiftDown()) {
                                deltaX += UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseShiftSensitivity;
                                deltaX += UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelShiftSensitivity;
                            } else {
                                deltaX += UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseSensitivity;
                                deltaX += UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelSensitivity;
                            }

                            if (deltaX >= SMBLWSettings.grabSnap || deltaX <= -SMBLWSettings.grabSnap) {
                                placeable.setPosition(placeable.getPosition().add(ProjectManager.getCurrentProject().modeDirection.multiply(
                                        SMBLWSettings.grabSnap * Math.round(deltaX / SMBLWSettings.grabSnap))));

                                deltaX = deltaX % SMBLWSettings.grabSnap;
                            }
                        } else if (Window.isShiftDown()) { //Precise movement with Shift
                            placeable.setPosition(placeable.getPosition().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseShiftSensitivity)));
                            placeable.setPosition(placeable.getPosition().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelShiftSensitivity)));
                        } else {
                            placeable.setPosition(placeable.getPosition().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseSensitivity)));
                            placeable.setPosition(placeable.getPosition().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelSensitivity)));
                        }
                    }
                }
                //</editor-fold>
            } else if (ProjectManager.getCurrentProject().mode == EnumActionMode.ROTATE_PLACEABLE) {
                //<editor-fold desc="Rotate">
                for (String key : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
                    Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(key);

                    if (placeable.getAsset().canRotate()) { //If can rotate
                        if (Window.isAltDown()) { //Snap with Alt
                            if (Window.isShiftDown()) {
                                deltaX += UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseShiftSensitivity;
                                deltaX += UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelShiftSensitivity;
                            } else {
                                deltaX += UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseSensitivity;
                                deltaX += UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelSensitivity;
                            }

                            if (deltaX >= SMBLWSettings.rotationSnap || deltaX <= -SMBLWSettings.rotationSnap) {
                                placeable.setRotation(placeable.getRotation().add(ProjectManager.getCurrentProject().modeDirection.multiply(
                                        SMBLWSettings.rotationSnap * Math.round(deltaX / SMBLWSettings.rotationSnap))));

                                deltaX = deltaX % SMBLWSettings.rotationSnap;
                            }
                        } else if (Window.isShiftDown()) { //Precise movement with Shift
                            placeable.setRotation(MathUtils.normalizeRotation(placeable.getRotation()
                                    .add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseShiftSensitivity))));
                            placeable.setRotation(MathUtils.normalizeRotation(placeable.getRotation()
                                    .add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelShiftSensitivity))));
                        } else {
                            placeable.setRotation(MathUtils.normalizeRotation(placeable.getRotation()
                                    .add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseSensitivity))));
                            placeable.setRotation(MathUtils.normalizeRotation(placeable.getRotation()
                                    .add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelSensitivity))));
                        }
                    }
                }
                //</editor-fold>
            } else if (ProjectManager.getCurrentProject().mode == EnumActionMode.SCALE_PLACEABLE) {
                //<editor-fold desc="Scale">
                for (String key : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
                    Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(key);

                    if (placeable.getAsset().canScale()) { //If can scale
                        if (Window.isAltDown()) { //Snap with Alt
                            if (Window.isShiftDown()) {
                                deltaX += UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseShiftSensitivity;
                                deltaX += UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelShiftSensitivity;
                            } else {
                                deltaX += UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseSensitivity;
                                deltaX += UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelSensitivity;
                            }

                            if (deltaX >= SMBLWSettings.scaleSnap || deltaX <= -SMBLWSettings.scaleSnap) {
                                placeable.setScale(placeable.getScale().add(ProjectManager.getCurrentProject().modeDirection.multiply(
                                        SMBLWSettings.scaleSnap * Math.round(deltaX / SMBLWSettings.scaleSnap))));

                                deltaX = deltaX % SMBLWSettings.scaleSnap;
                            }
                        } else if (Window.isShiftDown()) { //Precise movement with shift
                            placeable.setScale(placeable.getScale().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseShiftSensitivity)));
                            placeable.setScale(placeable.getScale().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelShiftSensitivity)));
                        } else {
                            placeable.setScale(placeable.getScale().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDelta().x * SMBLWSettings.modeMouseSensitivity)));
                            placeable.setScale(placeable.getScale().add(ProjectManager.getCurrentProject().modeDirection.multiply(UIUtils.getMouseDWheel() * SMBLWSettings.modeMouseWheelSensitivity)));
                        }
                    }
                }
                //</editor-fold>
            } else if (ProjectManager.getCurrentProject().mode == EnumActionMode.GRAB_KEYFRAME) {
                //TODO: Grab keyframes
//                //<editor-fold desc="Grab">
//                for (Map.Entry<String, BufferedAnimData> entry : ProjectManager.getCurrentClientLevelData().getAnimDataBufferMap().entrySet()) {
//                    BufferedAnimData bad = entry.getValue();
//
//                    if (Window.isAltDown()) { //Snap with Alt
//                        float snapToTranslation = Window.isShiftDown() ? SMBLWSettings.animSnapShift : SMBLWSettings.animSnap; //Precise movement with shift
//                        bad.setSnapToTranslation(snapToTranslation);
//                    } else {
//                        bad.setSnapToTranslation(null);
//                    }
//
//                    if (Window.isShiftDown()) { //Precise movement with Shift
//                        float newTranslation = (float) (bad.getKeyframeBufferTranslation() + UIUtils.getMouseDelta().x * SMBLWSettings.modeKeyframeMouseShiftSensitivity +
//                                UIUtils.getMouseDWheel() * SMBLWSettings.modeKeyframeMouseWheelShiftSensitivity);
//                        bad.setKeyframeBufferTranslation(newTranslation);
//                    } else {
//                        float newTranslation = (float) (bad.getKeyframeBufferTranslation() + UIUtils.getMouseDelta().x * SMBLWSettings.modeKeyframeMouseSensitivity +
//                                UIUtils.getMouseDWheel() * SMBLWSettings.modeKeyframeMouseWheelSensitivity);
//                        bad.setKeyframeBufferTranslation(newTranslation);
//                    }
//                }
//                //</editor-fold>
            } else if (ProjectManager.getCurrentProject().mode == EnumActionMode.SCALE_KEYFRAME) {
                //TODO: Scale keyframes
            }

            if (ProjectManager.getCurrentProject().mode != EnumActionMode.NONE) {
                updatePropertiesPlaceablesPanel();
            }
        }


        //<editor-fold desc="Set ProjectManager.getCurrentProject().mode Label">
        String modeStringKey;
        switch (ProjectManager.getCurrentProject().mode) {
            case NONE:
                modeStringKey = "none";
                break;
            case GRAB_PLACEABLE:
                modeStringKey = "grabPlaceable";
                break;
            case ROTATE_PLACEABLE:
                modeStringKey = "rotatePlaceable";
                break;
            case SCALE_PLACEABLE:
                modeStringKey = "scalePlaceable";
                break;
            case GRAB_KEYFRAME:
                modeStringKey = "grabKeyframe";
                break;
            case SCALE_KEYFRAME:
                modeStringKey = "scaleKeyframe";
                break;
            default:
                //This shouldn't happen
                modeStringKey = "invalid";
                break;
        }

        modeLabel.setText(String.format(LangManager.getItem("modeLabelFormat"), LangManager.getItem(modeStringKey)));
        //</editor-fold>

        //<editor-fold desc="ProjectManager.getCurrentProject().mode Direction Label">
        String modeDirectionString;
        if (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(1, 0, 0))) {
            modeDirectionString = LangManager.getItem("axisX");
        } else if (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(0, 1, 0))) {
            modeDirectionString = LangManager.getItem("axisY");
        } else if (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(0, 0, 1))) {
            modeDirectionString = LangManager.getItem("axisZ");
        } else if (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(1, 1, 1))) {
            modeDirectionString = LangManager.getItem("axisUniform");
        } else {
            modeDirectionString = String.format("%.2f, %.2f, %.2f", ProjectManager.getCurrentProject().modeDirection.x, ProjectManager.getCurrentProject().modeDirection.y, ProjectManager.getCurrentProject().modeDirection.z);
        }

        modeDirectionLabel.setText(String.format(LangManager.getItem("modeDirectionLabelFormat"), modeDirectionString));
        //</editor-fold>

        if (ProjectManager.getCurrentProject().mode == EnumActionMode.NONE) {
            modeCursor.setVisible(false);
        } else {
            modeCursor.setVisible(true);
        }

        if (Mouse.isButtonDown(2) ||
                ProjectManager.getCurrentProject().mode != EnumActionMode.NONE) {
            if (!Mouse.isGrabbed()) {
                Mouse.setGrabbed(true);
            }
        } else {
            if (Mouse.isGrabbed()) {
                Mouse.setGrabbed(false);
            }
        }

        Window.logOpenGLError("After MainScreen.preDraw()");

    }

    @Override
    public void draw() {
        Window.logOpenGLError("Before MainScreen.draw()");

        topLeftPos = new PosXY(0, 0);
        topLeftPx = new PosXY(0, 0);
        bottomRightPos = new PosXY(Display.getWidth(), Display.getHeight());
        bottomRightPx = new PosXY(Display.getWidth(), Display.getHeight());

        if (overlayUiScreen == null) {
            getParentMousePos();
        } else {
            mousePos = null;
        }

        preDraw();
        if (!preventRendering) {
            drawViewport();
        }
        postDraw();

        if (overlayUiScreen != null) {
            overlayUiScreen.draw();
        }

        Window.logOpenGLError("After MainScreen.draw()");
    }

    private void drawViewport() { //TODO: Draw per item group
        synchronized (renderingLock) {
            Window.logOpenGLError("Before MainScreen.drawViewport()");

            GL11.glEnable(GL11.GL_DEPTH_TEST);

            //<editor-fold desc="Setup the matrix">
            GL11.glViewport(0, 0, Display.getWidth(), Display.getHeight());
            GL11.glMatrixMode(GL11.GL_PROJECTION);
            GL11.glLoadIdentity();
            GLU.gluPerspective(90, Display.getWidth() / (float) Display.getHeight(), 0.5f, 1000f);
            //</editor-fold>

            GL11.glPushMatrix();

            Window.logOpenGLError("After MainScreen.drawViewport() - Matrix setup");

            GL11.glRotated(cameraRot.y, 1, 0, 0);
            GL11.glRotated(cameraRot.x, 0, 1, 0);

            GL11.glTranslated(-cameraPos.x, -cameraPos.y, -cameraPos.z);

            GL11.glLineWidth(2);

            //<editor-fold desc="Draw X line">
            UIColor.matRed().bindColor();
            GL11.glBegin(GL11.GL_LINES);
            GL11.glVertex3d(-10000, 0, 0);
            GL11.glVertex3d(10000, 0, 0);
            GL11.glEnd();
            //</editor-fold>

            //<editor-fold desc="Draw Z line">
            UIColor.matBlue().bindColor();
            GL11.glBegin(GL11.GL_LINES);
            GL11.glVertex3d(0, 0, -10000);
            GL11.glVertex3d(0, 0, 10000);
            GL11.glEnd();
            //</editor-fold>

            Window.logOpenGLError("After MainScreen.drawViewport() - Drawing global X & Z lines");

            GL11.glLineWidth(4);

            UIColor.pureWhite().bindColor();

            if (ProjectManager.getCurrentClientLevelData() != null) {
                //<editor-fold desc="Draw model with wireframes">
                GL11.glEnable(GL11.GL_DEPTH_TEST);

                ResourceShaderProgram currentShaderProgram = getCurrentShader();
                boolean useTextures = isCurrentShaderTextured();

                if (!SMBLWSettings.showTextures) {
                    UIColor.pureWhite().bindColor();
                }


                float time = ProjectManager.getCurrentClientLevelData().getTimelinePos();

                GL20.glUseProgram(currentShaderProgram.getProgramID());
                for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                    for (OBJObject object : model.scene.getObjectList()){
                        GL11.glPushMatrix();

                        //Transform at current time
                        transformObjectAtTime(object.name, time);

                        if (!ProjectManager.getCurrentClientLevelData().isObjectHidden(object.name)) {
                            model.drawModelObject(currentShaderProgram, useTextures, object.name);
                        }

                        GL11.glPopMatrix();
                    }
                }
                GL20.glUseProgram(0);

                Window.logOpenGLError("After MainScreen.drawViewport() - Drawing model filled");

                if (SMBLWSettings.showAllWireframes) {

                    for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                        for (OBJObject object : model.scene.getObjectList()) {
                            GL11.glPushMatrix();

                            transformObjectAtTime(object.name);

                            if (!ProjectManager.getCurrentClientLevelData().isObjectHidden(object.name)) {
                                GL11.glColor4f(0, 0, 0, 1);
                                model.drawModelObjectWireframe(null, false, object.name);

                                GL11.glDisable(GL11.GL_DEPTH_TEST);

                                GL11.glColor4f(0, 0, 0, 0.01f);
                                model.drawModelObjectWireframe(null, false, object.name);

                                GL11.glEnable(GL11.GL_DEPTH_TEST);
                            }

                            GL11.glPopMatrix();
                        }
                    }
                }
                Window.logOpenGLError("After MainScreen.drawViewport() - Drawing model wireframes");

                //</editor-fold>

                //<editor-fold desc="Draw placeables">
                List<DepthSortedPlaceable> depthSortedMap = new ArrayList<>();

                synchronized (ProjectManager.getCurrentLevelData().getPlacedObjects()) {

                    for (Map.Entry<String, Placeable> placeableEntry : ProjectManager.getCurrentLevelData().getPlacedObjects().entrySet()) {
                        Placeable placeable = placeableEntry.getValue();

                        double distance;

                        if (placeable.getAsset() instanceof AssetFalloutY) {
//                            distance = getDistance(cameraPos, new PosXYZ(cameraPos.x, placeable.getPosition().y, cameraPos.z));
                            distance = 0; //Always render the fallout plane last
                        } else {
                            distance = getDistance(cameraPos, placeable.getPosition());
                        }

                        depthSortedMap.add(new DepthSortedPlaceable(distance, placeableEntry));
                    }

                }

                Collections.sort(depthSortedMap, new DepthComparator());

                for (DepthSortedPlaceable placeableEntry : depthSortedMap) {
                    String name = placeableEntry.entry.getKey();
                    Placeable placeable = placeableEntry.entry.getValue();
                    boolean isSelected = ProjectManager.getCurrentClientLevelData().isPlaceableSelected(name);

                    drawPlaceable(placeable, isSelected);

                }
                //</editor-fold>

                //<editor-fold desc="Draw selected stuff">
                drawSelectedPlaceables(ProjectManager.getCurrentClientLevelData().getSelectedPlaceables(),
                        ProjectManager.getCurrentLevelData());

                //<editor-fold desc="Draw selected objects">
                UIColor.matBlue().bindColor();
                UIUtils.drawWithStencilOutside(
                        () -> {
                            for (String name : ProjectManager.getCurrentClientLevelData().getSelectedObjects()) {
                                GL11.glPushMatrix();

                                transformObjectAtTime(name);

                                if (!ProjectManager.getCurrentClientLevelData().isObjectHidden(name)) {
                                    for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                                        if (model.hasObject(name)) {
                                            model.drawModelObject(null, false, name);
                                        }
                                    }
                                }

                                GL11.glPopMatrix();
                            }
                        },
                        () -> {
                            GL11.glDisable(GL11.GL_DEPTH_TEST);
                            for (String name : ProjectManager.getCurrentClientLevelData().getSelectedObjects()) {
                                GL11.glPushMatrix();

                                transformObjectAtTime(name);

                                if (!ProjectManager.getCurrentClientLevelData().isObjectHidden(name)) {
                                    for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                                        if (model.hasObject(name)) {
                                            model.drawModelObjectWireframe(null, false, name);
                                        }
                                    }
                                }

                                GL11.glPopMatrix();
                            }
                            GL11.glEnable(GL11.GL_DEPTH_TEST);
                        });

                Window.logOpenGLError("After MainScreen.drawViewport() - Drawing model selection wireframes");
                //</editor-fold>
                //</editor-fold>

            }

            GL11.glPopMatrix();

            GL11.glColor3f(1, 1, 1);
            GL11.glDisable(GL11.GL_DEPTH_TEST);

            Window.setMatrix();

            Window.logOpenGLError("After MainScreen.drawViewport()");
        }
    }

    private void drawPlaceable(Placeable placeable, boolean isSelected) {
        ResourceModel model = placeable.getAsset().getModel();

        GL11.glPushMatrix();

        GL11.glTranslated(placeable.getPosition().x, placeable.getPosition().y, placeable.getPosition().z);
        GL11.glRotated(placeable.getRotation().z, 0, 0, 1);
        GL11.glRotated(placeable.getRotation().y, 0, 1, 0);
        GL11.glRotated(placeable.getRotation().x, 1, 0, 0);

        GL11.glEnable(GL11.GL_DEPTH_TEST);

        GL11.glScaled(placeable.getScale().x, placeable.getScale().y, placeable.getScale().z);
        GL20.glUseProgram(placeable.getAsset().getShaderProgram().getProgramID());
        model.drawModel(placeable.getAsset().getShaderProgram(), placeable.getAsset().isShaderTextured(), placeable.getAsset().getColor());
        GL20.glUseProgram(0);

        Window.logOpenGLError("After MainScreen.drawPlaceable() - Drawing placeable " + name + " filled");

        //<editor-fold desc="Draw blue wireframe and direction line if selected, else draw orange wireframe">
        GL11.glLineWidth(2);

        if (isSelected) {
            if (ProjectManager.getCurrentProject().mode.isPlaceableMode()) {
                GL11.glPushMatrix();

                GL11.glRotated(-placeable.getRotation().x, 1, 0, 0);
                GL11.glRotated(-placeable.getRotation().y, 0, 1, 0);
                GL11.glRotated(-placeable.getRotation().z, 0, 0, 1);

                if (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(1, 0, 0))) {
                    //<editor-fold desc="Draw X line">
                    UIColor.matRed(0.75).bindColor();
                    GL11.glBegin(GL11.GL_LINES);
                    GL11.glVertex3d(-10000, 0, 0);
                    GL11.glVertex3d(10000, 0, 0);
                    GL11.glEnd();
                    //</editor-fold>
                } else if (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(0, 1, 0))) {
                    //<editor-fold desc="Draw Y line">
                    UIColor.matGreen(0.75).bindColor();
                    GL11.glBegin(GL11.GL_LINES);
                    GL11.glVertex3d(0, -10000, 0);
                    GL11.glVertex3d(0, 10000, 0);
                    GL11.glEnd();
                    //</editor-fold>
                } else if (ProjectManager.getCurrentProject().modeDirection.equals(new PosXYZ(0, 0, 1))) {
                    //<editor-fold desc="Draw Z line">
                    UIColor.matBlue(0.75).bindColor();
                    GL11.glBegin(GL11.GL_LINES);
                    GL11.glVertex3d(0, 0, -10000);
                    GL11.glVertex3d(0, 0, 10000);
                    GL11.glEnd();
                    //</editor-fold>
                }

                GL11.glPopMatrix();
            }

            UIColor.matBlue().bindColor();
        } else {
            UIColor.matOrange().bindColor();
        }

        if (SMBLWSettings.showAllWireframes) {
            model.drawModelWireframe(null, false);
        }
        //</editor-fold>

        Window.logOpenGLError("After MainScreen.drawPlaceable() - Drawing placeable " + name + " wireframe (Depth test on)");

        GL11.glDisable(GL11.GL_DEPTH_TEST);

//        //<editor-fold desc="Draw blue wireframe if selected, else draw orange wireframe (Ignores depth test - Is semi transparent)">
//        if (isSelected) {
//            UIColor.matBlue(0.05).bindColor();
//        } else {
//            UIColor.matOrange(0.02).bindColor();
//        }
//
//        if (SMBLWSettings.showAllWireframes || isSelected) {
//            model.drawModelWireframe(null, false);
//        }
//        //</editor-fold>

        Window.logOpenGLError("After MainScreen.drawPlaceable() - Drawing placeable " + name + " wireframe (Depth test off)");

        GL11.glPopMatrix();
    }

    private void drawSelectedPlaceables(Collection<String> placeables, LevelData levelData) {
        UIColor.matBlue().bindColor();
        UIUtils.drawWithStencilOutside(
                () -> {
                    for (String name : placeables) {
                        Placeable placeable = levelData.getPlaceable(name);

                        GL11.glPushMatrix();

                        GL11.glTranslated(placeable.getPosition().x, placeable.getPosition().y, placeable.getPosition().z);
                        GL11.glRotated(placeable.getRotation().z, 0, 0, 1);
                        GL11.glRotated(placeable.getRotation().y, 0, 1, 0);
                        GL11.glRotated(placeable.getRotation().x, 1, 0, 0);
                        GL11.glScaled(placeable.getScale().x, placeable.getScale().y, placeable.getScale().z);

                        placeable.getAsset().getModel().drawModel(null, false);

                        GL11.glPopMatrix();
                    }
                },
                () -> {
                    GL11.glLineWidth(4);
                    GL11.glDisable(GL11.GL_DEPTH_TEST);
                    for (String name : placeables) {
                        Placeable placeable = levelData.getPlaceable(name);

                        GL11.glPushMatrix();

                        GL11.glTranslated(placeable.getPosition().x, placeable.getPosition().y, placeable.getPosition().z);
                        GL11.glRotated(placeable.getRotation().z, 0, 0, 1);
                        GL11.glRotated(placeable.getRotation().y, 0, 1, 0);
                        GL11.glRotated(placeable.getRotation().x, 1, 0, 0);
                        GL11.glScaled(placeable.getScale().x, placeable.getScale().y, placeable.getScale().z);

                        placeable.getAsset().getModel().drawModelWireframe(null, false);

                        GL11.glPopMatrix();
                    }
                    GL11.glEnable(GL11.GL_DEPTH_TEST);
                });
    }

    @Override
    public void onClick(int button, PosXY mousePos) {
        if (button == 0 && ProjectManager.getCurrentProject().mode != EnumActionMode.NONE) { //LMB: Confirm action
            confirmModeAction();
        } else if (button == 1 && ProjectManager.getCurrentProject().mode != EnumActionMode.NONE) { //RMB: Discard action
            discardModeAction();
        } else {
            super.onClick(button, mousePos);
        }
    }

    @Override
    public void onKeyDown(int key, char keyChar) {
        inputOverlay.onKeyDownManual(key, keyChar);

        if (overlayUiScreen != null) {
            overlayUiScreen.onKeyDown(key, keyChar);
        } else {

            boolean isTextFieldSelected = false;
            for (TextField textField : textFields) {
                if (textField.isSelected) {
                    isTextFieldSelected = true;
                    break;
                }
            }

            if (!isTextFieldSelected) {
                if (!Mouse.isButtonDown(2)) { //If MMB not down
                    if (key == Keyboard.KEY_F1) { //F1 to hide / show the ui
                        mainUI.setVisible(!mainUI.isVisible());

                    } else if (ProjectManager.getCurrentClientLevelData() != null) {
                        if (ProjectManager.getCurrentProject().mode == EnumActionMode.NONE) {
                            if (key == Keyboard.KEY_G) { //G: Grab
                                //TODO: Timeline keyframes
                                if (timeline.mouseOver) { //Grab keyframes when the cursor is over the timeline
//                                    addUndoCommand(new UndoModifyKeyframes(ProjectManager.getCurrentClientLevelData(), this,
//                                            ProjectManager.getCurrentLevelData().getObjectAnimDataMap()));
//                                    moveSelectedKeyframesToBuffer(false);
//                                    ProjectManager.getCurrentProject().mode = EnumActionMode.GRAB_KEYFRAME;
                                } else {
                                    addUndoCommand(new UndoAssetTransform(ProjectManager.getCurrentClientLevelData(), ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()));
                                    ProjectManager.getCurrentProject().mode = EnumActionMode.GRAB_PLACEABLE;
                                }
                            } else if (key == Keyboard.KEY_R) { //R: Rotate
                                if (!timeline.mouseOver) { //Do nothing if the cursor is over the timeline
                                    addUndoCommand(new UndoAssetTransform(ProjectManager.getCurrentClientLevelData(), ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()));
                                    ProjectManager.getCurrentProject().mode = EnumActionMode.ROTATE_PLACEABLE;
                                }
                            } else if (key == Keyboard.KEY_S) { //S: Scale
                                if (timeline.mouseOver) { //Scale keyframes when the cursor is over the timeline
//                                    addUndoCommand(new UndoModifyKeyframes(ProjectManager.getCurrentClientLevelData(), this,
//                                            ProjectManager.getCurrentLevelData().getObjectAnimDataMap()));
//                                    moveSelectedKeyframesToBuffer(false);
//                                    ProjectManager.getCurrentProject().mode = EnumActionMode.SCALE_KEYFRAME;
                                } else {
                                    addUndoCommand(new UndoAssetTransform(ProjectManager.getCurrentClientLevelData(), ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()));
                                    ProjectManager.getCurrentProject().mode = EnumActionMode.SCALE_PLACEABLE;
                                }

                            } else if (key == Keyboard.KEY_Z) { //Ctrl / Cmd Z: Undo - Ctrl / Cmd Shift Z: Redo
                                if (Window.isCtrlOrCmdDown()) {
                                    if (Window.isShiftDown()) {
                                        //Redo
                                        redo();
                                    } else {
                                        //Undo
                                        undo();
                                    }
                                }
                            } else if (key == Keyboard.KEY_Y) {
                                if (Window.isCtrlOrCmdDown()) {
                                    redo();
                                }

                            } else if (key == Keyboard.KEY_DELETE) { //Delete: Remove placeables / Remove keyframes if mouse over timeline
                                if (timeline.mouseOver) {
                                    //TODO: Keyframe deletion
//                                    if (
//                                            //Pos
//                                            ProjectManager.getCurrentClientLevelData().getSelectedPosXKeyframes().size() > 0 ||
//                                            ProjectManager.getCurrentClientLevelData().getSelectedPosYKeyframes().size() > 0 ||
//                                            ProjectManager.getCurrentClientLevelData().getSelectedPosZKeyframes().size() > 0 ||
//                                            //Rot
//                                            ProjectManager.getCurrentClientLevelData().getSelectedRotXKeyframes().size() > 0 ||
//                                            ProjectManager.getCurrentClientLevelData().getSelectedRotYKeyframes().size() > 0 ||
//                                            ProjectManager.getCurrentClientLevelData().getSelectedRotZKeyframes().size() > 0
//                                            ) {
//
//                                        addUndoCommand(new UndoModifyKeyframes(ProjectManager.getCurrentClientLevelData(), this,
//                                                ProjectManager.getCurrentLevelData().getObjectAnimDataMap()));
//
//                                        int keyframesRemoved = 0;
//
//                                        //Pos
//                                        for (KeyframeEntry entry : ProjectManager.getCurrentClientLevelData().getSelectedPosXKeyframes()) {
//                                            ProjectManager.getCurrentLevelData().getObjectAnimData(entry.getObjectName()).removePosXFrame(entry.getTime());
//                                            keyframesRemoved++;
//                                        }
//
//                                        for (KeyframeEntry entry : ProjectManager.getCurrentClientLevelData().getSelectedPosYKeyframes()) {
//                                            ProjectManager.getCurrentLevelData().getObjectAnimData(entry.getObjectName()).removePosYFrame(entry.getTime());
//                                            keyframesRemoved++;
//                                        }
//
//                                        for (KeyframeEntry entry : ProjectManager.getCurrentClientLevelData().getSelectedPosZKeyframes()) {
//                                            ProjectManager.getCurrentLevelData().getObjectAnimData(entry.getObjectName()).removePosZFrame(entry.getTime());
//                                            keyframesRemoved++;
//                                        }
//
//                                        //Rot
//                                        for (KeyframeEntry entry : ProjectManager.getCurrentClientLevelData().getSelectedRotXKeyframes()) {
//                                            ProjectManager.getCurrentLevelData().getObjectAnimData(entry.getObjectName()).removeRotXFrame(entry.getTime());
//                                            keyframesRemoved++;
//                                        }
//
//                                        for (KeyframeEntry entry : ProjectManager.getCurrentClientLevelData().getSelectedRotYKeyframes()) {
//                                            ProjectManager.getCurrentLevelData().getObjectAnimData(entry.getObjectName()).removeRotYFrame(entry.getTime());
//                                            keyframesRemoved++;
//                                        }
//
//                                        for (KeyframeEntry entry : ProjectManager.getCurrentClientLevelData().getSelectedRotZKeyframes()) {
//                                            ProjectManager.getCurrentLevelData().getObjectAnimData(entry.getObjectName()).removeRotZFrame(entry.getTime());
//                                            keyframesRemoved++;
//                                        }
//
//                                        ProjectManager.getCurrentClientLevelData().clearSelectedKeyframes();
//
//                                        if (keyframesRemoved > 1) {
//                                            sendNotif(String.format(LangManager.getItem("keyframeRemovedPlural"), keyframesRemoved));
//                                        } else {
//                                            sendNotif(LangManager.getItem("keyframeRemoved"));
//                                        }
//
//                                    } else {
//                                        sendNotif(LangManager.getItem("nothingSelected"));
//                                    }

                                } else {
                                    if (ProjectManager.getCurrentClientLevelData().getSelectedPlaceables().size() > 0) {

                                        List<String> toDelete = new ArrayList<>();

                                        for (String name : new HashSet<>(ProjectManager.getCurrentClientLevelData().getSelectedPlaceables())) {
                                            if (!(ProjectManager.getCurrentLevelData().getPlaceable(name).getAsset() instanceof AssetStartPos) && //Don't delete the start pos
                                                    !(ProjectManager.getCurrentLevelData().getPlaceable(name).getAsset() instanceof AssetFalloutY)) { //Don't delete the fallout Y
                                                toDelete.add(name);
                                            }
                                        }

                                        removePlaceables(toDelete);

                                        if (toDelete.size() > 1) {
                                            sendNotif(String.format(LangManager.getItem("placeableRemovedPlural"), toDelete.size()));
                                        } else {
                                            sendNotif(LangManager.getItem("placeableRemoved"));
                                        }

                                    } else {
                                        sendNotif(LangManager.getItem("nothingSelected"));
                                    }
                                }

                            } else if (key == Keyboard.KEY_D && Window.isCtrlOrCmdDown()) { //Ctrl / Cmd D: Duplicate

                                if (timeline.mouseOver) { //Duplicate keyframes when the cursor is over the timeline
                                    //TODO: Duplicate keyframes
                                    //TODO: Undo command
//                                    moveSelectedKeyframesToBuffer(true);
//                                    ProjectManager.getCurrentProject().mode = EnumActionMode.GRAB_KEYFRAME;

                                } else {
                                    Set<String> selectedPlaceables = new HashSet<>(ProjectManager.getCurrentClientLevelData().getSelectedPlaceables());
                                    ProjectManager.getCurrentClientLevelData().clearSelectedPlaceables();

                                    Map<String, Placeable> newPlaceables = new HashMap<>();

                                    int duplicated = 0;

                                    for (String name : selectedPlaceables) {
                                        Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(name);
                                        if (!(placeable.getAsset() instanceof AssetStartPos) &&
                                                !(placeable.getAsset() instanceof AssetFalloutY)) { //If the placeable isn't the start pos or fallout Y

                                            duplicated++;

                                            Placeable newPlaceable = placeable.getCopy();
                                            String newPlaceableName = ProjectManager.getCurrentLevelData().addPlaceable(newPlaceable);
                                            newPlaceables.put(newPlaceableName, newPlaceable);

                                            synchronized (outlinerPlaceablesListBoxLock) {
                                                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(newPlaceableName));
                                            }

                                            updateOutlinerPlaceablesPanel();

                                            ProjectManager.getCurrentClientLevelData().addSelectedPlaceable(newPlaceableName); //Select duplicated placeables
                                        }
                                    }

                                    if (duplicated > 0) {
                                        addUndoCommand(new UndoAddPlaceable(ProjectManager.getCurrentClientLevelData(), this, new ArrayList<>(newPlaceables.keySet()), new ArrayList<>(newPlaceables.values())));

                                        //Grab after duplicating
                                        addUndoCommand(new UndoAssetTransform(ProjectManager.getCurrentClientLevelData(), ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()));
                                        ProjectManager.getCurrentProject().mode = EnumActionMode.GRAB_PLACEABLE;
                                    }
                                }

                            } else if (key == Keyboard.KEY_SPACE) { //Spacebar: Play / pause animation
                                if (ProjectManager.getCurrentClientLevelData().getPlaybackSpeed() != 0) {
                                    ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(0.0f);
                                } else {
                                    ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(1.0f);
                                }

                            } else if (key == Keyboard.KEY_J) { //J: Rewind animation
                                if (Window.isAltDown()) { //Snap when holding alt
                                    float currentTime = ProjectManager.getCurrentClientLevelData().getTimelinePosSeconds();
                                    float snapTo = Window.isShiftDown() ? SMBLWSettings.animSnapShift : SMBLWSettings.animSnap;
                                    float newTime = currentTime - snapTo;
                                    float roundMultiplier = 1.0f / snapTo;
                                    float newTimeSnapped = Math.round(newTime * roundMultiplier) / roundMultiplier;

                                    ProjectManager.getCurrentClientLevelData().setTimelinePosSeconds(newTimeSnapped);

                                } else {
                                    float currentSpeed = ProjectManager.getCurrentClientLevelData().getPlaybackSpeed();

                                    if (currentSpeed > 1) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(currentSpeed * 0.5f);
                                    else if (currentSpeed < 0) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(currentSpeed * 2.0f);
                                    else if (currentSpeed == 0) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(-1.0f);
                                    else if (currentSpeed > 0) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(0.0f);
                                }

                            } else if (key == Keyboard.KEY_K) { //K: Stop animation
                                ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(0.0f);

                            } else if (key == Keyboard.KEY_L) { //L: Forward animation

                                if (Window.isAltDown()) { //Snap when holding alt
                                    float currentTime = ProjectManager.getCurrentClientLevelData().getTimelinePosSeconds();
                                    float snapTo = Window.isShiftDown() ? SMBLWSettings.animSnapShift : SMBLWSettings.animSnap;
                                    float newTime = currentTime + snapTo;
                                    float roundMultiplier = 1.0f / snapTo;
                                    float newTimeSnapped = Math.round(newTime * roundMultiplier) / roundMultiplier;

                                    ProjectManager.getCurrentClientLevelData().setTimelinePosSeconds(newTimeSnapped);

                                } else {
                                    float currentSpeed = ProjectManager.getCurrentClientLevelData().getPlaybackSpeed();

                                    if (currentSpeed < -1) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(currentSpeed * 0.5f);
                                    else if (currentSpeed > 0) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(currentSpeed * 2.0f);
                                    else if (currentSpeed == 0) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(1.0f);
                                    else if (currentSpeed < 0) ProjectManager.getCurrentClientLevelData().setPlaybackSpeed(0.0f);
                                }

                            } else {
                                super.onKeyDown(key, keyChar);
                            }

                        } else if (key == Keyboard.KEY_X) { //X Axis
                            ProjectManager.getCurrentProject().modeDirection = new PosXYZ(1, 0, 0);
                            modeCursor.setColor(UIColor.matRed());
                        } else if (key == Keyboard.KEY_Y) { //Y Axis
                            ProjectManager.getCurrentProject().modeDirection = new PosXYZ(0, 1, 0);
                            modeCursor.setColor(UIColor.matGreen());
                        } else if (key == Keyboard.KEY_Z) { //Z Axis
                            ProjectManager.getCurrentProject().modeDirection = new PosXYZ(0, 0, 1);
                            modeCursor.setColor(UIColor.matBlue());
                        } else if (key == Keyboard.KEY_U) { //XYZ (Uniform)
                            ProjectManager.getCurrentProject().modeDirection = new PosXYZ(1, 1, 1);
                            modeCursor.setColor(UIColor.matWhite());

                        } else if (key == Keyboard.KEY_ESCAPE) {
                            discardModeAction();
                        } else if (key == Keyboard.KEY_RETURN) {
                            confirmModeAction();

                        } else {
                            super.onKeyDown(key, keyChar);
                        }
                    } else {
                        super.onKeyDown(key, keyChar);
                    }
                }
            } else {
                super.onKeyDown(key, keyChar);

                if (key == Keyboard.KEY_ESCAPE) { //Deselect text fields on escape
                    for (TextField textField : textFields) {
                        textField.setSelected(false);
                    }
                }
            }
        }

    }

    @Override
    public void onKeyReleased(int key, char keyChar) {
        inputOverlay.onKeyReleasedManual(key, keyChar);

        super.onKeyReleased(key, keyChar);
    }

    private void confirmModeAction() {
        ProjectManager.getCurrentProject().mode = EnumActionMode.NONE;
        assert ProjectManager.getCurrentClientLevelData() != null;
        commitBufferedKeyframes();
        updatePropertiesPlaceablesPanel();
        deltaX = 0; //Reset deltaX when no ProjectManager.getCurrentProject().mode is active
    }

    private void discardModeAction() {
        ProjectManager.getCurrentProject().mode = EnumActionMode.NONE;
        undo();
        discardBufferedKeyframes();
        deltaX = 0; //Reset deltaX when no ProjectManager.getCurrentProject().mode is active
    }

    public void sendNotif(String message) {
        sendNotif(message, UIColor.matGrey900());
    }

    public void sendNotif(String message, UIColor color) {
        for (Map.Entry<String, Component> entry : notifPanel.childComponents.entrySet()) {
            assert entry.getValue().plugins.get(1) instanceof NotificationPlugin;
            ((NotificationPlugin) entry.getValue().plugins.get(1)).time = 1.5;
        }

        final Panel panel = new Panel();
        panel.setOnInitAction(() -> {
            panel.setTopLeftPos(260, -80 - TIMELINE_HEIGHT);
            panel.setBottomRightPos(-260, -56 - TIMELINE_HEIGHT);
            panel.setTopLeftAnchor(0, 1.2);
            panel.setBottomRightAnchor(1, 1.2);
            panel.setBackgroundColor(color.alpha(0.75));
        });
        PluginSmoothAnimateAnchor animateAnchor = new PluginSmoothAnimateAnchor();
        panel.addPlugin(animateAnchor);
        panel.addPlugin(new NotificationPlugin(animateAnchor));
        notifPanel.addChildComponent("notificationPanel" + String.valueOf(notificationID), panel);

        final Label label = new Label();
        label.setOnInitAction(() -> {
            label.setTopLeftPos(4, 0);
            label.setBottomRightPos(-4, 0);
            label.setTopLeftAnchor(0, 0);
            label.setBottomRightAnchor(1, 1);
            label.setText(message);
        });
        panel.addChildComponent("label", label);

        notificationID++;
    }

    private void addPlaceable(Placeable placeable) {
        if (ProjectManager.getCurrentClientLevelData() != null) {
            String name = ProjectManager.getCurrentLevelData().addPlaceable(placeable);
            ProjectManager.getCurrentClientLevelData().clearSelectedPlaceables();
            ProjectManager.getCurrentClientLevelData().addSelectedPlaceable(name);

            addUndoCommand(new UndoAddPlaceable(ProjectManager.getCurrentClientLevelData(), this, Collections.singletonList(name), Collections.singletonList(placeable)));

            synchronized (outlinerPlaceablesListBoxLock) {
                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(name));
            }

            updateOutlinerPlaceablesPanel();
        } else {
            sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
        }
    }

    private void removePlaceable(String name) {
        if (ProjectManager.getCurrentClientLevelData() != null) {

            addUndoCommand(new UndoRemovePlaceable(ProjectManager.getCurrentClientLevelData(), this, Collections.singletonList(name), Collections.singletonList(ProjectManager.getCurrentLevelData().getPlaceable(name))));

            ProjectManager.getCurrentClientLevelData().removeSelectedPlaceable(name);
            ProjectManager.getCurrentLevelData().removePlaceable(name);

            synchronized (outlinerPlaceablesListBoxLock) {
                outlinerPlaceablesListBox.removeChildComponent(name + "OutlinerPlaceable");
            }
        } else {
            sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
        }
    }

    private void removePlaceables(List<String> names) {
        if (ProjectManager.getCurrentClientLevelData() != null) {

            List<Placeable> placeables = new ArrayList<>();
            for (String name : names) {
                placeables.add(ProjectManager.getCurrentLevelData().getPlaceable(name));
            }

            assert ProjectManager.getCurrentClientLevelData() != null;
            addUndoCommand(new UndoRemovePlaceable(ProjectManager.getCurrentClientLevelData(), this, names, placeables));

            for (String name : names) {
                ProjectManager.getCurrentClientLevelData().removeSelectedPlaceable(name);
                ProjectManager.getCurrentLevelData().removePlaceable(name);

                synchronized (outlinerPlaceablesListBoxLock) {
                    outlinerPlaceablesListBox.removeChildComponent(name + "OutlinerPlaceable");
                }
            }
        } else {
            sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
        }
    }

    private void newLevelData(Set<ExternalModel> externalModels, boolean replace) throws IOException {
        synchronized (renderingLock) {
            try {
                preventRendering = true;

                Window.drawable.makeCurrent();

                GL11.glFinish();

                if (!replace) {
                    //Clear outliner list box
                    synchronized (outlinerPlaceablesListBoxLock) {
                        outlinerPlaceablesListBox.clearChildComponents();
                    }

                    //Reset undo history
                    undoCommandList.clear();
                    redoCommandList.clear();
                }

                if (ProjectManager.getCurrentClientLevelData() != null) {
                    //Unload textures and VBOs
                    ProjectManager.getCurrentLevelData().unloadAllModels();
                }

                if (!replace) {
                    ProjectManager.getCurrentProject().clientLevelData = new ClientLevelData();
                    ProjectManager.getCurrentClientLevelData().setOnSelectedPlaceablesChanged(this::onSelectedPlaceablesChanged);
                    ProjectManager.getCurrentClientLevelData().setOnSelectedObjectsChanged(this::onSelectedObjectsChanged);
                    ProjectManager.getCurrentClientLevelData().setOnSelectedExternalBackgroundObjectsChanged(this::onSelectedExternalBackgroundObjectsChanged);
                    ProjectManager.getCurrentClientLevelData().setOnTimelinePosChanged(this::onTimelinePosChanged);
                }

                ProjectManager.getCurrentLevelData().clearModelObjSources();

                for (ExternalModel extModel : externalModels) {
                    ResourceModel model = OBJLoader.loadModel(extModel.file.getPath());
                    ProjectManager.getCurrentLevelData().addModel(model);
                    ProjectManager.getCurrentLevelData().addModelObjSource(extModel.file);
                }

                synchronized (ProjectManager.getCurrentLevelData().getPlacedObjects()) {

                    Placeable startPosPlaceable;
                    String startPosPlaceableName = null;
                    Placeable falloutYPlaceable;
                    String falloutYPlaceableName = null;
                    if (!replace) {
                        startPosPlaceable = new Placeable(new AssetStartPos());
                        startPosPlaceable.setPosition(new PosXYZ(0, 1, 0));
                        startPosPlaceableName = ProjectManager.getCurrentLevelData().addPlaceable(
                                LangManager.getItem("assetStartPos"), startPosPlaceable, "STAGE_RESERVED");

                        if (objectMode == EnumObjectMode.PLACEABLE_EDIT) {
                            ProjectManager.getCurrentClientLevelData().addSelectedPlaceable(startPosPlaceableName);
                        }

                        falloutYPlaceable = new Placeable(new AssetFalloutY());
                        falloutYPlaceable.setPosition(new PosXYZ(0, -10, 0));
                        falloutYPlaceableName = ProjectManager.getCurrentLevelData().addPlaceable(
                                LangManager.getItem("assetFalloutY"), falloutYPlaceable, "STAGE_RESERVED");

                    }

                    Window.drawable.makeCurrent();

                    if (!replace) {
                        synchronized (outlinerPlaceablesListBoxLock) {
                            outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(startPosPlaceableName));
                            outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(falloutYPlaceableName));
                        }

                        updateOutlinerPlaceablesPanel();
                    }

                }

                if (replace) {
                    //Remove selected objects if they no longer exist in the new OBJ
                    for (String name : ProjectManager.getCurrentClientLevelData().getSelectedObjects()) {
                        for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                            if (!model.hasObject(name)) {
                                ProjectManager.getCurrentClientLevelData().removeSelectedObject(name);
                            }
                        }
                    }

                    //Remove objects if they no longer exist in the new OBJ
                    Set<String> allObjectNames = new HashSet<>();
                    for (Map.Entry<String, WSItemGroup> entry : ProjectManager.getCurrentLevelData().getItemGroupMap().entrySet()) {
                        allObjectNames.addAll(entry.getValue().getObjectNames());
                    }
                    for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                        for (OBJObject object : model.scene.getObjectList()) {
                            if (!allObjectNames.contains(object.name)) {
                                ProjectManager.getCurrentLevelData().removeObject(name);
                            }
                        }
                    }

                } else {
                    ProjectManager.getCurrentClientLevelData().clearSelectedObjects();
                    //TODO: Update timeline
//                    timeline.setObjectAnimDataMap(ProjectManager.getCurrentLevelData().getObjectAnimDataMap());
                }

                if (!OBJLoader.isLastObjTriangulated) {
                    setOverlayUiScreen(new DialogOverlayUIScreen(LangManager.getItem("warning"), LangManager.getItem("notTriangulated")));
                }

                synchronized (outlinerObjectsListBoxLock) {
                    outlinerObjectsListBox.clearChildComponents();

                    ProjectManager.getCurrentClientLevelData().clearHiddenObjects(); //Unhide all objects

                    for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                        for (OBJObject object : model.scene.getObjectList()) {
                            outlinerObjectsListBox.addChildComponent(getOutlinerObjectComponent(object.name));
                        }
                    }
                }

                if (replace) {
                    //Replace all external background objects in the objects outliner that were removed
                    for (String name : ProjectManager.getCurrentLevelData().getBackgroundExternalObjects()) {
                        outlinerObjectsListBox.addChildComponent(getOutlinerExternalBackgroundObjectComponent(name));
                    }

                    //Delete all specified level models that no longer exist, and add new level models to the first item group
                    Set<String> existingModels = new HashSet<>();
                    for (Map.Entry<String, WSItemGroup> entry : ProjectManager.getCurrentLevelData().getItemGroupMap().entrySet()) {
                        WSItemGroup itemGroup = entry.getValue();

                        Set<String> removedObjects = new HashSet<>();
                        for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                            for (String name : itemGroup.getObjectNames())
                            if (!model.hasObject(name)) {
                                removedObjects.add(name);
                            }
                        }
                        itemGroup.removeObjects(removedObjects);

                        existingModels.addAll(itemGroup.getObjectNames());
                    }
                    existingModels.addAll(ProjectManager.getCurrentLevelData().getBackgroundObjects());

                    //Fetch a list of all new models
                    Set<String> newModels = new HashSet<>();
                    for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                        for (OBJObject obj : model.scene.getObjectList()) {
                            if (!existingModels.contains(obj.name)) {
                                //It's new
                                newModels.add(obj.name);
                            }
                        }
                    }

                    //Add the new models to the first item group
                    ProjectManager.getCurrentLevelData().getFirstItemGroup().addObjects(newModels);
                } else {
                    //Not replacing - Add everything to the first item group
                    for (ResourceModel model : ProjectManager.getCurrentLevelData().getModels()) {
                        for (OBJObject obj : model.scene.getObjectList()) {
                            ProjectManager.getCurrentLevelData().getFirstItemGroup().addObject(obj.name);
                        }
                    }
                }

                //Update the timeline
                timeline.updatePercent(ProjectManager.getCurrentClientLevelData().getTimelinePos());
                timeline.updateMaxAndLeadInTime(ProjectManager.getCurrentLevelData().getMaxTime(),
                        ProjectManager.getCurrentLevelData().getLeadInTime());

                GL11.glFlush();

                Window.drawable.releaseContext();
            } catch (LWJGLException e) {
                LogHelper.error(getClass(), e);
            }

            preventRendering = false;
        }
    }

    public void addUndoCommand(UndoCommand undoCommand) {
        undoCommandList.add(undoCommand);
        redoCommandList.clear();
    }

    private void undo() {
        if (undoCommandList.size() > 0) {
            redoCommandList.add(undoCommandList.get(undoCommandList.size() - 1).getRedoCommand());
            undoCommandList.get(undoCommandList.size() - 1).undo();
            sendNotif(undoCommandList.get(undoCommandList.size() - 1).getUndoMessage());
            undoCommandList.remove(undoCommandList.size() - 1);

            updatePropertiesPlaceablesPanel();
            updatePropertiesObjectsPanel();
            updateOutlinerPlaceablesPanel();
            updateOutlinerObjectsPanel();
        }

    }

    private void redo() {
        if (redoCommandList.size() > 0) {
            undoCommandList.add(redoCommandList.get(redoCommandList.size() - 1).getRedoCommand());
            redoCommandList.get(redoCommandList.size() - 1).undo();
            sendNotif(redoCommandList.get(redoCommandList.size() - 1).getRedoMessage());
            redoCommandList.remove(redoCommandList.size() - 1);

            updatePropertiesPlaceablesPanel();
            updatePropertiesObjectsPanel();
            updateOutlinerPlaceablesPanel();
            updateOutlinerObjectsPanel();
        }
    }

    public Component getOutlinerPlaceableComponent(String name) {
        final ItemButton placeableButton = new ItemButton();
        placeableButton.setOnInitAction(() -> {
            placeableButton.setTopLeftPos(0, 0);
            placeableButton.setBottomRightPos(0, 18);
            placeableButton.setId(name);
            placeableButton.setItemGroupCol(ProjectManager.getCurrentLevelData().getPlaceableItemGroup(name).getColor());
        });
        placeableButton.setOnLMBAction(() -> {
            assert ProjectManager.getCurrentClientLevelData() != null;

            if (Window.isShiftDown()) { //Toggle selection on shift
                ProjectManager.getCurrentClientLevelData().toggleSelectedPlaceable(name);
            } else {
                ProjectManager.getCurrentClientLevelData().clearSelectedExternalBackgroundObjects();
                ProjectManager.getCurrentClientLevelData().clearSelectedPlaceables();
                ProjectManager.getCurrentClientLevelData().addSelectedPlaceable(name);
            }
        });
        placeableButton.setName(name + "OutlinerPlaceable");

        return placeableButton;
    }

    public Component getOutlinerObjectComponent(String name) {
        final OutlinerObject outlinerObject = new OutlinerObject(name);
        outlinerObject.setOnInitAction(() -> {
            outlinerObject.setTopLeftPos(0, 0);
            outlinerObject.setBottomRightPos(0, 18);
        });

        return outlinerObject;
    }

    public Component getOutlinerExternalBackgroundObjectComponent(String name) {
        final TextButton objectButton = new TextButton();
        objectButton.setOnInitAction(() -> {
            objectButton.setTopLeftPos(0, 0);
            objectButton.setBottomRightPos(0, 18);
            objectButton.setText(name);
            objectButton.setBackgroundIdleColor(UIColor.matPink());
        });
        objectButton.setOnLMBAction(() -> {
            assert ProjectManager.getCurrentClientLevelData() != null;

            if (Window.isShiftDown()) { //Toggle selection on shift
                ProjectManager.getCurrentClientLevelData().toggleSelectedExternalBackgroundObject(name);
            } else {
                ProjectManager.getCurrentClientLevelData().clearSelectedExternalBackgroundObjects();
                ProjectManager.getCurrentClientLevelData().clearSelectedObjects();
                ProjectManager.getCurrentClientLevelData().addSelectedExternalBackgroundObject(name);
            }
        });
        objectButton.setName(name + "OutlinerObject");

        return objectButton;
    }

    private void importObj() {
        if (!isLoadingProject) {
            new Thread(() -> {
                isLoadingProject = true;
                importObjButton.setEnabled(false);
                importConfigButton.setEnabled(false);
                exportButton.setEnabled(false);
                settingsButton.setEnabled(false);
                projectSettingsButton.setEnabled(false);
                FileDialog fd = new FileDialog((Frame) null);
                fd.setMode(FileDialog.LOAD);
                fd.setFilenameFilter((dir, filename) -> filename.toUpperCase().endsWith(".OBJ"));
                fd.setVisible(true);

                File[] files = fd.getFiles();
                if (files != null && files.length > 0) {
                    File file = files[0];
                    LogHelper.info(getClass(), "Opening file: " + file.getAbsolutePath());

                    try {
                        if (ProjectManager.getCurrentClientLevelData() != null) {
                            AskReplaceObjOverlayUIScreen dialog = new AskReplaceObjOverlayUIScreen();
                            setOverlayUiScreen(dialog);
                            boolean shouldRepalce = dialog.waitForShouldReplaceResponse();
                            newLevelData(Collections.singleton(new ExternalModel(file, ModelType.OBJ)), shouldRepalce);
                        } else {
                            newLevelData(Collections.singleton(new ExternalModel(file, ModelType.OBJ)), false);
                        }

                        updateOutlinerObjectsPanel();
                    } catch (IOException e) {
                        LogHelper.error(getClass(), "Failed to open file");
                        LogHelper.error(getClass(), e);
                    }
                }
                projectSettingsButton.setEnabled(true);
                settingsButton.setEnabled(true);
                exportButton.setEnabled(true);
                importConfigButton.setEnabled(true);
                importObjButton.setEnabled(true);
                isLoadingProject = false;
            }, "ObjFileOpenThread").start();
        } else {
            LogHelper.warn(getClass(), "Tried importing OBJ when already importing OBJ");
        }
    }

    private void export() {
        if (!isLoadingProject) {
            if (ProjectManager.getCurrentClientLevelData() != null) {
                setOverlayUiScreen(new ExportOverlayUIScreen());
            } else {
                sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
            }
        }
    }

    private void onSelectedPlaceablesChanged() {
        updatePropertiesPlaceablesPanel();
        updateOutlinerPlaceablesPanel();
    }

    public void updatePropertiesPlaceablesPanel() {
        double posAvgX = 0;
        boolean canGrabX = false;
        double posAvgY = 0;
        boolean canGrabY = false;
        double posAvgZ = 0;
        boolean canGrabZ = false;

        double rotAvgX = 0;
        double rotAvgY = 0;
        double rotAvgZ = 0;
        boolean canRotate = false;

        double sclAvgX = 0;
        double sclAvgY = 0;
        double sclAvgZ = 0;
        boolean canScale = false;

        assert ProjectManager.getCurrentClientLevelData() != null;

        Class<?> selectedIAsset;
        if (ProjectManager.getCurrentClientLevelData().getSelectedPlaceables().size() > 0) {
            selectedIAsset = ProjectManager.getCurrentLevelData().getPlaceable(ProjectManager.getCurrentClientLevelData().getSelectedPlaceables().iterator().next()).getAsset().getClass();
        } else {
            selectedIAsset = null;
        }

        for (String name : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
            Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(name);

            if (placeable.getAsset().canGrabX()) {
                canGrabX = true;
                posAvgX += placeable.getPosition().x;
            }
            if (placeable.getAsset().canGrabY()) {
                canGrabY = true;
                posAvgY += placeable.getPosition().y;
            }
            if (placeable.getAsset().canGrabZ()) {
                canGrabZ = true;
                posAvgZ += placeable.getPosition().z;
            }

            if (placeable.getAsset().canRotate()) {
                canRotate = true;

                rotAvgX += placeable.getRotation().x;
                rotAvgY += placeable.getRotation().y;
                rotAvgZ += placeable.getRotation().z;
            }

            if (placeable.getAsset().canScale()) {
                canScale = true;

                sclAvgX += placeable.getScale().x;
                sclAvgY += placeable.getScale().y;
                sclAvgZ += placeable.getScale().z;
            } else {
                sclAvgX += 1;
                sclAvgY += 1;
                sclAvgZ += 1;
            }

            if (selectedIAsset != null && !placeable.getAsset().getClass().isAssignableFrom(selectedIAsset)) {
                selectedIAsset = null;
            }
        }

        int selectedCount = ProjectManager.getCurrentClientLevelData().getSelectedPlaceables().size();

        if (selectedCount != 0) {
            posAvgX = posAvgX / (double) selectedCount;
            posAvgY = posAvgY / (double) selectedCount;
            posAvgZ = posAvgZ / (double) selectedCount;

            if (canGrabX) {
                placeablePositionTextFields.setXEnabled(true);
            } else {
                placeablePositionTextFields.setXEnabled(false);
                placeablePositionTextFields.setXValue(0);
            }
            if (canGrabY) {
                placeablePositionTextFields.setYEnabled(true);
            } else {
                placeablePositionTextFields.setYEnabled(false);
                placeablePositionTextFields.setYValue(0);
            }
            if (canGrabZ) {
                placeablePositionTextFields.setZEnabled(true);
            } else {
                placeablePositionTextFields.setZEnabled(false);
                placeablePositionTextFields.setZValue(0);
            }

            placeablePositionTextFields.setXValue(posAvgX);
            placeablePositionTextFields.setYValue(posAvgY);
            placeablePositionTextFields.setZValue(posAvgZ);
        } else {
            placeablePositionTextFields.setXEnabled(false);
            placeablePositionTextFields.setYEnabled(false);
            placeablePositionTextFields.setZEnabled(false);

            placeablePositionTextFields.setXValue(0);
            placeablePositionTextFields.setYValue(0);
            placeablePositionTextFields.setZValue(0);
        }

        if (selectedCount != 0 && canRotate) {
            rotAvgX = rotAvgX / (double) selectedCount;
            rotAvgY = rotAvgY / (double) selectedCount;
            rotAvgZ = rotAvgZ / (double) selectedCount;

            placeableRotationTextFields.setXEnabled(true);
            placeableRotationTextFields.setYEnabled(true);
            placeableRotationTextFields.setZEnabled(true);

            placeableRotationTextFields.setXValue(rotAvgX);
            placeableRotationTextFields.setYValue(rotAvgY);
            placeableRotationTextFields.setZValue(rotAvgZ);
        } else {
            placeableRotationTextFields.setXEnabled(false);
            placeableRotationTextFields.setYEnabled(false);
            placeableRotationTextFields.setZEnabled(false);

            placeableRotationTextFields.setXValue(0);
            placeableRotationTextFields.setYValue(0);
            placeableRotationTextFields.setZValue(0);
        }

        if (selectedCount != 0 && canScale) {
            sclAvgX = sclAvgX / (double) selectedCount;
            sclAvgY = sclAvgY / (double) selectedCount;
            sclAvgZ = sclAvgZ / (double) selectedCount;

            placeableScaleTextFields.setXEnabled(true);
            placeableScaleTextFields.setYEnabled(true);
            placeableScaleTextFields.setZEnabled(true);

            placeableScaleTextFields.setXValue(sclAvgX);
            placeableScaleTextFields.setYValue(sclAvgY);
            placeableScaleTextFields.setZValue(sclAvgZ);
        } else {
            placeableScaleTextFields.setXEnabled(false);
            placeableScaleTextFields.setYEnabled(false);
            placeableScaleTextFields.setZEnabled(false);

            placeableScaleTextFields.setXValue(1);
            placeableScaleTextFields.setYValue(1);
            placeableScaleTextFields.setZValue(1);
        }

        if (selectedCount > 0) {
            boolean stageReservedOnly = true;
            for (String name : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
                String igName = ProjectManager.getCurrentLevelData().getPlaceableItemGroupName(name);
                if (!Objects.equals(igName, "STAGE_RESERVED")) {
                    stageReservedOnly = false;
                    break;
                }
            }

            placeableItemGroupButton.setEnabled(!stageReservedOnly);

            String commonItemGroup = null; //The name of an item group if all selected placeables have belong to the same item group, or null if the don't

            for (String name : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
                String igName = ProjectManager.getCurrentLevelData().getPlaceableItemGroupName(name);
                if (commonItemGroup == null) {
                    commonItemGroup = igName;
                }

                if (!Objects.equals(commonItemGroup, igName)) {
                    commonItemGroup = null;
                    break;
                }
            }

            if (commonItemGroup != null) {
                placeableItemGroupButton.setText(commonItemGroup);
            } else {
                placeableItemGroupButton.setText("...");
            }
        } else {
            placeableItemGroupButton.setEnabled(false);
            placeableItemGroupButton.setText(LangManager.getItem("nothingSelected"));
        }

        if (selectedIAsset != null) {
            String[] types = ProjectManager.getCurrentLevelData().getPlaceable(ProjectManager.getCurrentClientLevelData().getSelectedPlaceables().iterator().next()).getAsset().getValidTypes();

            if (types != null) {
                typeList = Arrays.asList(types);
                typeButton.setText(LangManager.getItem(ProjectManager.getCurrentLevelData().getPlaceable(ProjectManager.getCurrentClientLevelData().getSelectedPlaceables().iterator().next()).getAsset().getType()));
                typeButton.setEnabled(true);
            } else {
                typeList = null;
                typeButton.setText(LangManager.getItem("noTypes"));
                typeButton.setEnabled(false);
            }
        } else {
            typeList = null;
            typeButton.setText(LangManager.getItem("noTypes"));
            typeButton.setEnabled(false);
        }

    }

    private void onSelectedObjectsChanged() {
        updatePropertiesObjectsPanel();
        updateOutlinerObjectsPanel();
        timeline.setSelectedObjects(ProjectManager.getCurrentClientLevelData().getSelectedObjects());
    }

    public void updatePropertiesObjectsPanel() {
        int selectedCount = ProjectManager.getCurrentClientLevelData().getSelectedObjects().size();
        if (selectedCount > 0) {
            boolean stageReservedOnly = true;
            for (String name : ProjectManager.getCurrentClientLevelData().getSelectedObjects()) {
                String igName = ProjectManager.getCurrentLevelData().getObjectItemGroupName(name);
                if (!Objects.equals(igName, "STAGE_RESERVED")) {
                    stageReservedOnly = false;
                    break;
                }
            }

            objectItemGroupButton.setEnabled(!stageReservedOnly);

            String commonItemGroup = null; //The name of an item group if all selected objects have belong to the same item group, or null if the don't

            for (String name : ProjectManager.getCurrentClientLevelData().getSelectedObjects()) {
                String igName = ProjectManager.getCurrentLevelData().getObjectItemGroupName(name);
                if (commonItemGroup == null) {
                    commonItemGroup = igName;
                }

                if (!Objects.equals(commonItemGroup, igName)) {
                    commonItemGroup = null;
                    break;
                }
            }

            if (commonItemGroup != null) {
                objectItemGroupButton.setText(commonItemGroup);
            } else {
                objectItemGroupButton.setText("...");
            }
        } else {
            objectItemGroupButton.setEnabled(false);
            objectItemGroupButton.setText(LangManager.getItem("nothingSelected"));
        }
    }

    private void updateOutlinerPlaceablesPanel() {
        if (ProjectManager.getCurrentClientLevelData() != null) {
            //<editor-fold desc="Darken selected placeables in the outliner">
            synchronized (outlinerPlaceablesListBoxLock) {
                for (Map.Entry<String, Component> entry : outlinerPlaceablesListBox.childComponents.entrySet()) {
                    ItemButton button = (ItemButton) entry.getValue();

                    button.setItemGroupCol(ProjectManager.getCurrentLevelData().getPlaceableItemGroup(button.getId()).getColor());

                    if (ProjectManager.getCurrentClientLevelData().isPlaceableSelected(button.getId())) {
//                        button.setBackgroundIdleColor(UIColor.matBlue900());
                        button.setSelected(true);
                    } else {
//                        button.setBackgroundIdleColor(UIColor.matBlue());
                        button.setSelected(false);
                    }
                }
            }
            //</editor-fold>
        }
    }

    private void updateOutlinerObjectsPanel() {
        //<editor-fold desc="Darken selected object in the outliner">
        synchronized (outlinerObjectsListBoxLock) {
            for (Map.Entry<String, Component> entry : outlinerObjectsListBox.childComponents.entrySet()) {
                assert entry.getValue() instanceof OutlinerObject;
                OutlinerObject outlinerObject = (OutlinerObject) entry.getValue();

                outlinerObject.setButtonColor(getOutlinerObjectColor(outlinerObject.getObjectName()));
            }
        }
        //</editor-fold>
    }

    private void onSelectedExternalBackgroundObjectsChanged() {
        updatePropertiesObjectsPanel();
        updateOutlinerObjectsPanel();
    }

    @Deprecated
    private UIColor getOutlinerObjectColor(String name) {
        if (ProjectManager.getCurrentClientLevelData().isObjectSelected(name)) {
            return UIColor.matBlue900();
        } else {
            return UIColor.matBlue();
        }
//        if (ProjectManager.getCurrentLevelData().isObjectBackground(name)) {
//            if (ProjectManager.getCurrentClientLevelData().isObjectSelected(name)) {
//                return UIColor.matPurple900();
//            } else {
//                return UIColor.matPurple();
//            }
//        } else if (ProjectManager.getCurrentLevelData().isObjectBackgroundExternal(name)) {
//            if (ProjectManager.getCurrentClientLevelData().isExternalBackgroundObjectSelected(name)) {
//                return UIColor.matPink900();
//            } else {
//                return UIColor.matPink();
//            }
//        } else if (ProjectManager.getCurrentLevelData().doesObjectHaveAnimData(name)) {
//            if (ProjectManager.getCurrentClientLevelData().isObjectSelected(name)) {
//                return UIColor.matGreen900();
//            } else {
//                return UIColor.matGreen();
//            }
//        } else {
//            if (ProjectManager.getCurrentClientLevelData().isObjectSelected(name)) {
//                return UIColor.matBlue900();
//            } else {
//                return UIColor.matBlue();
//            }
//
    }

    private IUIScreen getTypeSelectorOverlayScreen(double mouseY) {
        final double mousePercentY = mouseY / Display.getHeight();

        return new TypeSelectorOverlayScreen(mousePercentY, typeList);
    }

    private IUIScreen getItemGroupSelectorOverlayScreen(double mouseX, double mouseY) {
        final double mousePercentY = mouseY / Display.getHeight();

        return new ItemGroupSelectorOverlayScreen(mousePercentY, ProjectManager.getCurrentLevelData().getItemGroupMap());
    }

    public void setTypeForSelectedPlaceables(String type) {
        boolean changed = false;

        assert ProjectManager.getCurrentClientLevelData() != null;
        for (String name : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
            Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(name);

            if (!Objects.equals(placeable.getAsset().getType(), type)) {
                changed = true;
            }
        }

        if (changed) {
            addUndoCommand(new UndoAssetTypeChange(ProjectManager.getCurrentClientLevelData(), ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()));
        }

        for (String name : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
            Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(name);

            placeable.getAsset().setType(type);
        }

        updatePropertiesPlaceablesPanel();
    }

    public void setItemGroupForSelectedPlaceables(String itemGroup) {
        boolean changed = false;

        assert ProjectManager.getCurrentClientLevelData() != null;
        for (String name : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
            if (!Objects.equals(ProjectManager.getCurrentLevelData().getPlaceableItemGroupName(name), itemGroup)) {
                changed = true;
            }
        }

        if (changed) {
            addUndoCommand(new UndoPlaceableItemGroupChange(ProjectManager.getCurrentClientLevelData(), ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()));
        }

        for (String name : ProjectManager.getCurrentClientLevelData().getSelectedPlaceables()) {
            Placeable placeable = ProjectManager.getCurrentLevelData().getPlaceable(name);

            if (placeable.getAsset() instanceof AssetStartPos || placeable.getAsset() instanceof AssetFalloutY) continue; //Start pos and fallout Y cannot change item groups

            ProjectManager.getCurrentLevelData().changePlaceableItemGroup(name, itemGroup);
        }

        updateOutlinerPlaceablesPanel();
        updatePropertiesPlaceablesPanel();
    }

    public void setItemGroupForSelectedObjects(String itemGroup) {
        boolean changed = false;

        assert ProjectManager.getCurrentClientLevelData() != null;
        for (String name : ProjectManager.getCurrentClientLevelData().getSelectedObjects()) {
            if (!Objects.equals(ProjectManager.getCurrentLevelData().getObjectItemGroupName(name), itemGroup)) {
                changed = true;
            }
        }

        if (changed) {
            addUndoCommand(new UndoObjectItemGroupChange(ProjectManager.getCurrentClientLevelData(), ProjectManager.getCurrentClientLevelData().getSelectedObjects()));
        }

        for (String name : ProjectManager.getCurrentClientLevelData().getSelectedObjects()) {
            ProjectManager.getCurrentLevelData().changeObjectItemGroup(name, itemGroup);
        }

        updateOutlinerObjectsPanel();
        updatePropertiesObjectsPanel();
    }

    private void showSettings() {
        setOverlayUiScreen(new SettingsOverlayUIScreen());
    }

    private void showProjectSettings() {
        if (ProjectManager.getCurrentProject() != null && ProjectManager.getCurrentClientLevelData() != null) {
            setOverlayUiScreen(new ProjectSettingsOverlayUIScreen());
        } else {
            sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
        }
    }

    private ResourceShaderProgram getCurrentShader() {
        if (SMBLWSettings.showTextures) {
            if (SMBLWSettings.isUnlit) {
                return ResourceManager.getShaderProgram("texUnlitShaderProgram");
            } else {
                return ResourceManager.getShaderProgram("texShaderProgram");
            }
        } else {
            if (SMBLWSettings.isUnlit) {
                return ResourceManager.getShaderProgram("colUnlitShaderProgram");
            } else {
                return ResourceManager.getShaderProgram("colShaderProgram");
            }
        }
    }

    private boolean isCurrentShaderTextured() {
        return SMBLWSettings.showTextures;
    }

    private double getDistance(PosXYZ p1, PosXYZ p2) {
        return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2) + Math.pow(p2.z - p1.z, 2));
    }

    private void importConfig() {
        boolean xmlOnly;

        //Only allow XML configs when no OBJ loaded
        if (ProjectManager.getCurrentProject() != null && ProjectManager.getCurrentClientLevelData() != null) {
            xmlOnly = false;
        } else {
            xmlOnly = true;
//            sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
        }

        if (!isLoadingProject) {
            new Thread(() -> {
                isLoadingProject = true;
                importObjButton.setEnabled(false);
                importConfigButton.setEnabled(false);
                exportButton.setEnabled(false);
                settingsButton.setEnabled(false);
                projectSettingsButton.setEnabled(false);
                FileDialog fd = new FileDialog((Frame) null);
                fd.setMode(FileDialog.LOAD);
                if (xmlOnly) {
                    fd.setFilenameFilter((dir, filename) -> filename.toUpperCase().endsWith(".XML"));
                } else {
                    fd.setFilenameFilter((dir, filename) -> filename.toUpperCase().endsWith(".TXT") || filename.toUpperCase().endsWith(".XML"));
                }
                fd.setVisible(true);

                File[] files = fd.getFiles();
                if (files != null && files.length > 0) {
                    File file = files[0];
                    LogHelper.info(getClass(), "Opening file: " + file.getAbsolutePath());

                    try {
                        ConfigData configData = new ConfigData();
                        if (file.getName().toUpperCase().endsWith(".XML")) {
                            //Assume XML config file
                            XMLConfigParser.parseConfig(configData, file);

                            if (ProjectManager.getCurrentClientLevelData() != null) {
                                newLevelData(configData.models, true);
                            } else {
                                newLevelData(configData.models, false); //No client level data - Don't replace as there's nothing to replace
                            }


                        } else {
                            //Assume smbcnv config file
                            SMBCnvConfigParser.parseConfig(configData, file);
                        }

                        ClientLevelData cld = ProjectManager.getCurrentClientLevelData();
                        LevelData ld = cld.getLevelData();

                        synchronized (ProjectManager.getCurrentLevelData().getPlacedObjects()) {

                            //TODO: Replace with clear item groups instead

                            cld.clearSelectedPlaceables();
                            ld.clearPlacedObjects();
                            ld.clearBackgroundObjects();
                            outlinerPlaceablesListBox.clearChildComponents();

                            cld.clearSelectedKeyframes();

                            //Add start pos
                            if (configData.startList.size() > 0) {
                                Start start = configData.startList.entrySet().iterator().next().getValue();
                                Placeable startPlaceable = new Placeable(new AssetStartPos());
                                startPlaceable.setPosition(new PosXYZ(start.pos.x, start.pos.y, start.pos.z));
                                startPlaceable.setRotation(new PosXYZ(start.rot.x, start.rot.y, start.rot.z));
                                String name = ld.addPlaceable(LangManager.getItem("assetStartPos"), startPlaceable, "STAGE_RESERVED");
                                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(name));
                            } else {
                                //No start found - use default start
                                Placeable startPlaceable = new Placeable(new AssetStartPos());
                                startPlaceable.setPosition(new PosXYZ(0, 1, 0));
                                String name = ld.addPlaceable(LangManager.getItem("assetStartPos"), startPlaceable, "STAGE_RESERVED");
                                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(name));
                            }

                            //Add fallout y
                            Placeable falloutPlaceable = new Placeable(new AssetFalloutY());
                            falloutPlaceable.setPosition(new PosXYZ(0, configData.falloutPlane, 0));
                            String falloutName = ld.addPlaceable(LangManager.getItem("assetFalloutY"), falloutPlaceable, "STAGE_RESERVED");
                            outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(falloutName));

                            //Add goals
                            for (Map.Entry<String, Goal> entry : configData.getFirstItemGroup().goalList.entrySet()) { //TODO: Forced to use static item group
                                Goal goal = entry.getValue();
                                Placeable goalPlaceable = new Placeable(new AssetGoal());
                                goalPlaceable.setPosition(new PosXYZ(goal.pos));
                                goalPlaceable.setRotation(new PosXYZ(goal.rot));

                                String type = "blueGoal";
                                //Type 0 = blueGoal
                                if (goal.type == Goal.EnumGoalType.GREEN) {
                                    type = "greenGoal";
                                } else if (goal.type == Goal.EnumGoalType.RED) {
                                    type = "redGoal";
                                }

                                goalPlaceable.getAsset().setType(type);
                                String name = ld.addPlaceable(entry.getKey(), goalPlaceable);
                                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(name));
                            }

                            //Add bumpers
                            for (Map.Entry<String, Bumper> entry : configData.getFirstItemGroup().bumperList.entrySet()) { //TODO: Forced to use static item group
                                Bumper bumper = entry.getValue();
                                Placeable bumperPlaceable = new Placeable(new AssetBumper());
                                bumperPlaceable.setPosition(new PosXYZ(bumper.pos));
                                bumperPlaceable.setRotation(new PosXYZ(bumper.rot));
                                bumperPlaceable.setScale(new PosXYZ(bumper.scl));

                                String name = ld.addPlaceable(entry.getKey(), bumperPlaceable);
                                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(name));
                            }

                            //Add jamabars
                            for (Map.Entry<String, Jamabar> entry : configData.getFirstItemGroup().jamabarList.entrySet()) { //TODO: Forced to use static item group
                                Jamabar jamabar = entry.getValue();
                                Placeable jamabarPlaceable = new Placeable(new AssetJamabar());
                                jamabarPlaceable.setPosition(new PosXYZ(jamabar.pos));
                                jamabarPlaceable.setRotation(new PosXYZ(jamabar.rot));
                                jamabarPlaceable.setScale(new PosXYZ(jamabar.scl));

                                String name = ld.addPlaceable(entry.getKey(), jamabarPlaceable);
                                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(name));
                            }

                            //Add bananas
                            for (Map.Entry<String, Banana> entry : configData.getFirstItemGroup().bananaList.entrySet()) { //TODO: Forced to use static item group
                                Banana banana = entry.getValue();
                                Placeable bananaPlaceable = new Placeable(new AssetBanana());
                                bananaPlaceable.setPosition(new PosXYZ(banana.pos));

                                String type = "singleBanana";
                                //Type 0 = singleBanana
                                if (banana.type == Banana.EnumBananaType.BUNCH) {
                                    type = "bunchBanana";
                                }

                                bananaPlaceable.getAsset().setType(type);
                                String name = ld.addPlaceable(entry.getKey(), bananaPlaceable);
                                outlinerPlaceablesListBox.addChildComponent(getOutlinerPlaceableComponent(name));
                            }

                            //TODO: Add wormholes and fallout volumes

                            //Mark background objects
                            for (String name : configData.backgroundList) {
                                for (ResourceModel model : ld.getModels()) {
                                    if (model.hasObject(name)) {
                                        ld.changeObjectItemGroup(name, "BACKGROUND_RESERVED");
                                    }
                                }
                            }

                            //Set max time
                            ld.setMaxTime(configData.maxTime);
                            ld.setLeadInTime(configData.leadInTime);

                            //Add anim data //TODO: Revamp for item groups
//                                for (Map.Entry<String, ConfigAnimData> entry : configData.animDataMap.entrySet()) {
//                                    if (ld.getModel().hasObject(entry.getValue().getObjectName())) {
//                                        ld.setAnimData(entry.getValue().getObjectName(), new AnimData(entry.getValue()));
//                                    }
//                                }

                        }

                        updateOutlinerPlaceablesPanel();
                        updateOutlinerObjectsPanel();
                    } catch (IOException e) {
                        LogHelper.error(getClass(), "Failed to open file");
                        LogHelper.error(getClass(), e);
                    } catch (SAXException | ParserConfigurationException e) {
                        LogHelper.error(getClass(), "Failed to parse XML file");
                        LogHelper.error(getClass(), e);
                    }
                }
                projectSettingsButton.setEnabled(true);
                settingsButton.setEnabled(true);
                exportButton.setEnabled(true);
                importConfigButton.setEnabled(true);
                importObjButton.setEnabled(true);
                isLoadingProject = false;
            }, "ConfigFileOpenThread").start();
        } else {
            LogHelper.warn(getClass(), "Tried importing OBJ when already importing OBJ");
        }
    }

    private void onTimelinePosChanged(Float percent) {
        timeline.updatePercent(percent);
//        if (SMBLWSettings.autoUpdateProperties) {
//            assert ProjectManager.getCurrentClientLevelData() != null;
//            if (ProjectManager.getCurrentClientLevelData().getTimelinePos() != percent) {
//                ProjectManager.getCurrentClientLevelData().clearCurrentFrameObjectAnimData();
//            }
//
//            updatePropertiesObjectsPanel();
//        } else {
//            for (Map.Entry<String, AnimData> entry : ProjectManager.getCurrentClientLevelData().getCurrentFrameObjectAnimDataMap().entrySet()) {
//                entry.getValue().moveFirstFrame(percent);
//            }
//        }
    }

    public void addTextField(TextField textField) {
        if (textFields == null) {
            textFields = new HashSet<>();
        }

        textFields.add(textField);
    }

    private void transformObjectAtTime(String name, float time) {
        //TODO
//        ITransformable transform = ProjectManager.getCurrentClientLevelData().getObjectNamedTransform(name, time);
//        PosXYZ translate = transform.getPosition();
//        PosXYZ rotate = transform.getRotation();
//        GL11.glTranslated(translate.x, translate.y, translate.z);
//        GL11.glRotated(rotate.z, 0, 0, 1);
//        GL11.glRotated(rotate.y, 0, 1, 0);
//        GL11.glRotated(rotate.x, 1, 0, 0);
    }

    private void transformObjectAtTime(String name) {
        transformObjectAtTime(name, ProjectManager.getCurrentClientLevelData().getTimelinePos());
    }

    private void addNextFrameAction(UIAction action) {
        nextFrameActions.add(action);
    }

    @Deprecated
    private void moveSelectedKeyframesToBuffer(boolean makeCopy) {
    }

    @Deprecated
    private void commitBufferedKeyframes() {
    }

    @Deprecated
    private void discardBufferedKeyframes() {

    }

} public class MathUtils {

    public static double lerp(double a, double b, double f) {
        return a + f * (b - a);
    }

    /**
     * Linearly interpolates between two {@link UIColor}s
     *
     * @param a The starting {@link UIColor}
     * @param b The ending {@link UIColor}
     * @param f The percentage between the two {@link UIColor}
     * @return The {@link UIColor} which if f% between a and b
     */
    public static UIColor lerpUIColor(UIColor a, UIColor b, float f) {
        return new UIColor(a.r * 255 + f * (b.r * 255 - a.r * 255),
                a.g * 255 + f * (b.g * 255 - a.g * 255),
                a.b * 255 + f * (b.b * 255 - a.b * 255),
                a.a * 255 + f * (b.a * 255 - a.a * 255));
    }

    public static double clamp(double val, double min, double max) {
        return Math.max(min, Math.min(max, val));
    }

    public static PosXYZ normalizeRotation(PosXYZ rot) {
        while (rot.x >= 360) {
            rot.x -= 360;
        }

        while (rot.y >= 360) {
            rot.y -= 360;
        }

        while (rot.z >= 360) {
            rot.z -= 360;
        }

        while (rot.x < 0) {
            rot.x += 360;
        }

        while (rot.y < 0) {
            rot.y += 360;
        }

        while (rot.z < 0) {
            rot.z += 360;
        }

        return rot;
    }

    public static boolean isInRange(double val, double min, double max) {
        return val >= min && val <= max;
    }

    public static float snapTo(float val, float snapTo) {
        float roundMultiplier = 1.0f / snapTo;
        return Math.round(val * roundMultiplier) / roundMultiplier;
    }

    /**
     * @param t A value from 0 to 1
     * @return Quadratic interpolated value between 0 and 1
     */
    public static float cubicEaseInOut(float t) {
        return t < 0.5f ? 4 * t * t * t : (t - 1f) * (2f * t - 2f) * (2f * t - 2f) + 1f;
    }

}
import craftedcart.smblevelworkshop.SMBLWSettings;
import craftedcart.smblevelworkshop.Window;
import craftedcart.smblevelworkshop.animation.AnimData;
import craftedcart.smblevelworkshop.animation.BufferedAnimData;
import craftedcart.smblevelworkshop.level.ClientLevelData;
import craftedcart.smblevelworkshop.project.ProjectManager;
import craftedcart.smblevelworkshop.resource.LangManager;
import craftedcart.smblevelworkshop.resource.ResourceManager;
import craftedcart.smblevelworkshop.ui.DefaultUITheme;
import craftedcart.smblevelworkshop.ui.MainScreen;
import craftedcart.smblevelworkshop.util.MathUtils;
import io.github.craftedcart.fluidui.component.*;
import io.github.craftedcart.fluidui.plugin.AbstractComponentPlugin;
import io.github.craftedcart.fluidui.util.EnumHAlignment;
import io.github.craftedcart.fluidui.util.PosXY;
import io.github.craftedcart.fluidui.util.UIColor;
import io.github.craftedcart.fluidui.util.UIUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.input.Mouse;
import org.newdawn.slick.opengl.Texture;
import java.text.DecimalFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
                        ProjectManager.getCurrentLevelData().getLeadInTime());
            }
        });
        mainScreen.addTextField(maxTimeTextField);
        headingPanel.addChildComponent("maxTimeTextField", maxTimeTextField);

        final Label secondsOutOfLabel = new Label();
        secondsOutOfLabel.setOnInitAction(() -> {
            secondsOutOfLabel.setTopLeftPos(-200, 0);
            secondsOutOfLabel.setBottomRightPos(-176, 0);
            secondsOutOfLabel.setTopLeftAnchor(1, 0);
            secondsOutOfLabel.setBottomRightAnchor(1, 1);
            secondsOutOfLabel.setText(LangManager.getItem("secondsOutOfAbbr"));
        });
        headingPanel.addChildComponent("secondsOutOfLabel", secondsOutOfLabel);

        //Defined at class level
        timeTextField.setOnInitAction(() -> {
            timeTextField.setTopLeftPos(-264, 0);
            timeTextField.setBottomRightPos(-200, 0);
            timeTextField.setTopLeftAnchor(1, 0);
            timeTextField.setBottomRightAnchor(1, 1);
            timeTextField.setInputRegexCheck("[0-9.-]");
            timeTextField.setValue(df.format(0));
        });
        timeTextField.setOnReturnAction(() -> timeTextField.setSelected(false));
        timeTextField.setOnValueConfirmedAction(() -> {
            try {
                float newValue = Float.parseFloat(timeTextField.value);
                if (ProjectManager.getCurrentClientLevelData() != null) {
                    if (newValue >= -ProjectManager.getCurrentLevelData().getLeadInTime()) {
                        if (newValue <= ProjectManager.getCurrentLevelData().getMaxTime()) {
                            ProjectManager.getCurrentClientLevelData().setTimelinePosSeconds(newValue);
                        } else {
                            mainScreen.sendNotif(LangManager.getItem("numberMustBeLessThanMaxTime"), UIColor.matRed());
                        }
                    } else {
                        mainScreen.sendNotif(String.format(LangManager.getItem("numberMustBeGreaterThanOrEqualTo"),
                                -ProjectManager.getCurrentLevelData().getLeadInTime()), UIColor.matRed());
                    }
                } else {
                    mainScreen.sendNotif(LangManager.getItem("noLevelLoaded"), UIColor.matRed());
                }
            } catch (NumberFormatException e) {
                mainScreen.sendNotif(LangManager.getItem("invalidNumber"), UIColor.matRed());
            }

            if (ProjectManager.getCurrentClientLevelData() != null) {
                updatePercent(ProjectManager.getCurrentClientLevelData().getTimelinePos());
            }
        });
        mainScreen.addTextField(timeTextField);
        headingPanel.addChildComponent("timeTextField", timeTextField);

    }

    public void updatePercent(Float percent) {
        float seconds =
                percent *
                        (ProjectManager.getCurrentLevelData().getMaxTime() + ProjectManager.getCurrentLevelData().getLeadInTime()) -
                        ProjectManager.getCurrentLevelData().getLeadInTime();

        posPanel.setTopLeftAnchor(percent, 0);
        posPanel.setBottomRightAnchor(percent, 1);
        posPanel.setRightText(df.format(percent * 100) + "%");
        posPanel.setLeftText(df.format(seconds) + secondsSuffix);

        percentTextField.setValue(df.format(percent * 100));
        timeTextField.setValue(df.format(
                percent *
                (ProjectManager.getCurrentLevelData().getMaxTime() + ProjectManager.getCurrentLevelData().getLeadInTime()) -
                ProjectManager.getCurrentLevelData().getLeadInTime()
        ));
    }

    public void updateMaxAndLeadInTime(float maxTime, float leadInTime) {
        maxTimeTextField.setValue(df.format(maxTime));
        posPanel.setLeftText(df.format(ProjectManager.getCurrentClientLevelData().getTimelinePos() * (maxTime + leadInTime) - leadInTime) + secondsSuffix);
    }

    private class TimelinePlugin extends AbstractComponentPlugin {

        private PosXY selectionStartPos = new PosXY();

        @Override
        public void onPreDraw() {
            if (linkedComponent.mouseOver) {

                if (Mouse.isButtonDown(1)) { //If RMB down

                    //Calculate top left and bottom right pos of selection
                    PosXY topLeftPos = new PosXY(
                            Math.min(selectionStartPos.x, linkedComponent.mousePos.x),
                            Math.min(selectionStartPos.y, linkedComponent.mousePos.y));
                    PosXY bottomRightPos = new PosXY(
                            Math.max(selectionStartPos.x, linkedComponent.mousePos.x),
                            Math.max(selectionStartPos.y, linkedComponent.mousePos.y));
                    
                    //Draw selection box
                    UIUtils.drawQuad(topLeftPos, new PosXY(bottomRightPos.x, bottomRightPos.y), UIColor.matBlue(0.25));
                    UIUtils.drawQuad(topLeftPos, new PosXY(bottomRightPos.x, topLeftPos.y + 2), UIColor.matBlue());
                    UIUtils.drawQuad(topLeftPos, new PosXY(topLeftPos.x + 2, bottomRightPos.y), UIColor.matBlue());
                    UIUtils.drawQuad(new PosXY(bottomRightPos.x - 2, topLeftPos.y),
                            new PosXY(bottomRightPos.x, bottomRightPos.y), UIColor.matBlue());
                    UIUtils.drawQuad(new PosXY(topLeftPos.x, bottomRightPos.y - 2), bottomRightPos, UIColor.matBlue());

                    cursorPosPanel.setVisible(false);

                } else {
                    //<editor-fold desc="Manage cursorPosPanel">
                    float percent = 0.0f;
                    float seconds = 0.0f;
                    if (linkedComponent.mousePos != null) {
                        percent = (float) MathUtils.clamp(((linkedComponent.mousePos.x - linkedComponent.topLeftPx.x) /
                                        (linkedComponent.width - (TIMELINE_PADDING * 2))) - TIMELINE_PADDING / linkedComponent.width,
                                0, 1);

                        if (ProjectManager.getCurrentClientLevelData() != null) {
                            seconds = ProjectManager.getCurrentClientLevelData().timelinePercentToSeconds(percent);

                            if (Window.isAltDown()) { //Alt to snap - Shift for precision