pl.kotcrab.jdialogue.editor.Renderer.java Source code

Java tutorial

Introduction

Here is the source code for pl.kotcrab.jdialogue.editor.Renderer.java

Source

/*******************************************************************************
 * DialogueEditor
 * Copyright (C) 2013-2014 Pawel Pastuszak
 * <p>
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * <p>
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package pl.kotcrab.jdialogue.editor;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Preferences;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.input.GestureDetector;
import com.badlogic.gdx.input.GestureDetector.GestureListener;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.thoughtworks.xstream.XStream;
import pl.kotcrab.jdialogue.editor.components.ConnectionRenderer;
import pl.kotcrab.jdialogue.editor.components.Connector;
import pl.kotcrab.jdialogue.editor.components.DComponent;
import pl.kotcrab.jdialogue.editor.components.DComponentType;
import pl.kotcrab.jdialogue.editor.components.types.CallbackCheckComponent;
import pl.kotcrab.jdialogue.editor.components.types.CallbackComponent;
import pl.kotcrab.jdialogue.editor.components.types.ChoiceComponent;
import pl.kotcrab.jdialogue.editor.components.types.EndComponent;
import pl.kotcrab.jdialogue.editor.components.types.RandomComponent;
import pl.kotcrab.jdialogue.editor.components.types.RelayComponent;
import pl.kotcrab.jdialogue.editor.components.types.StartComponent;
import pl.kotcrab.jdialogue.editor.components.types.TextComponent;
import pl.kotcrab.jdialogue.editor.project.Project;

import javax.swing.JOptionPane;
import java.util.ArrayList;
import java.util.Iterator;

//TODO przesuwanie kamery, gdy przy rysowaniu zblizymy kursor do krawdzie ekranu, uyteczna funkcja
public class Renderer implements ApplicationListener, InputProcessor, GestureListener {
    private EditorListener listener;
    private Preferences prefs;

    private OrthographicCamera camera;
    private ShapeRenderer shapeRenderer;
    private SpriteBatch batch;
    private Rectangle cameraRect;

    private ArrayList<DComponent> componentList = new ArrayList<DComponent>();

    private ArrayList<ArrayList<DComponent>> undoList = new ArrayList<ArrayList<DComponent>>();
    private ArrayList<ArrayList<DComponent>> redoList = new ArrayList<ArrayList<DComponent>>();

    private ArrayList<DComponent> clipboardList;

    private Connector selectedConnector = null;
    private ConnectionRenderer connectionRenderer;

    private DComponent selectedComponent = null;
    private ArrayList<DComponent> selectedComponentsList = new ArrayList<DComponent>();
    private int attachPointX;
    private int attachPointY;

    private RectangularSelection rectangularSelection;

    private boolean disposed = false;

    private boolean renderDebug = false;
    private Matrix4 renderDebugMatrix = new Matrix4();
    private Matrix4 renderInfoTextMatrix = new Matrix4();

    private KotcrabText infoText;

    private Project project;

    private XStream xstream;

    private boolean dirty;

    private int panCounter = 0;
    private boolean cameraDragged;

    public Renderer(EditorListener listener, XStream xstream) {
        this.listener = listener;
        this.xstream = xstream;
    }

    @Override
    public void create() {
        Assets.load();

        prefs = Gdx.app.getPreferences("pl.kotcrab.dialoguelib.editorprefs");
        App.setPrefs(prefs);
        App.loadPrefs();

        camera = new OrthographicCamera(1280, 720);
        camera.position.x = 1280 / 2;
        camera.position.y = 720 / 2;
        Touch.setCamera(camera);
        cameraRect = CameraUtils.calcCameraBoundingRectangle(camera);

        shapeRenderer = new ShapeRenderer();
        batch = new SpriteBatch();

        rectangularSelection = new RectangularSelection(new RectangularSelectionListener() {
            @Override
            public void finishedDrawing(ArrayList<DComponent> matchingComponents) {
                selectedComponentsList = matchingComponents;
            }
        }, componentList);

        InputMultiplexer mul = new InputMultiplexer();
        mul.addProcessor(this);
        mul.addProcessor(new GestureDetector(this));
        mul.addProcessor(rectangularSelection);
        Gdx.input.setInputProcessor(mul);

        connectionRenderer = new ConnectionRenderer();

        infoText = new KotcrabText(Assets.consolasFont, "Load or create new project to begin!", false, 0, 0);
        infoText.setScale(1.4f);

    }

    public void resetCamera() {
        // camera.zoom = 1;
        camera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        camera.position.x = Gdx.graphics.getWidth() / 2;
        camera.position.y = Gdx.graphics.getHeight() / 2;
        camera.zoom = 1;
    }

    public void update() {
        camera.update();
        cameraRect = CameraUtils.calcCameraBoundingRectangle(camera);

        shapeRenderer.setProjectionMatrix(camera.combined);
        batch.setProjectionMatrix(camera.combined);

        for (DComponent comp : componentList) {
            comp.calcIfVisible(cameraRect);
        }

        // this is here for simplicity, because we are using a key and a mouse button
        // keyDown, will only provide us with key pressed event, and we will have to create flags for keyPressed and ButtonPressed
        if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) && selectedConnector != null
                && Gdx.input.isButtonPressed(Buttons.LEFT)) {
            selectedConnector.detach();
        }
    }

    @Override
    public void render() {
        if (!disposed) {
            update();

            int renderedComponents = 0;
            int renderedConnections = 0;

            Gdx.gl.glClearColor(0.69f, 0.69f, 0.69f, 1);
            Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

            // why more loops == better
            // we can draw more shapes/sprites in single batch, wich reduces lwgjl nSwapBuffers and imroves perofmence

            shapeRenderer.begin(ShapeType.Filled);
            for (DComponent comp : componentList) {
                if (comp.isVisible()) {
                    comp.renderShapes(shapeRenderer);
                    renderedComponents++;
                }
            }
            shapeRenderer.end();

            shapeRenderer.begin(ShapeType.Line);
            for (DComponent comp : componentList) {
                if (comp.isVisible())
                    comp.renderSelectionOutline(shapeRenderer, Color.BLACK);
            }
            shapeRenderer.end();

            batch.setShader(Assets.fontDistanceFieldShader);
            batch.begin();
            for (DComponent comp : componentList) {
                comp.render(batch);
            }
            batch.end();
            batch.setShader(null);

            shapeRenderer.begin(ShapeType.Line);
            if (selectedComponent != null)
                selectedComponent.renderSelectionOutline(shapeRenderer, Color.ORANGE); // 1
            shapeRenderer.end();

            // these if's are difrrent, easy to omit... (1,2)

            if (selectedConnector != null) // 2
            {
                if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT))
                    selectedConnector.renderAsSelected(shapeRenderer, Color.RED);
                else
                    selectedConnector.renderAsSelected(shapeRenderer, Color.ORANGE);

                if (Gdx.input.isButtonPressed(Buttons.LEFT)) {
                    connectionRenderer.render(shapeRenderer, selectedConnector, Touch.getX(), Touch.getY());
                }
            }

            rectangularSelection.render(shapeRenderer);

            shapeRenderer.begin(ShapeType.Line);
            for (DComponent comp : selectedComponentsList) {
                comp.renderSelectionOutline(shapeRenderer, Color.RED);
            }
            shapeRenderer.end();

            connectionRenderer.setCameraCalc(cameraRect);

            shapeRenderer.setColor(Color.BLACK);
            shapeRenderer.begin(ShapeType.Line);
            for (DComponent comp : componentList) {
                renderedConnections += connectionRenderer.renderLines(shapeRenderer, comp,
                        Gdx.input.isKeyPressed(Keys.CONTROL_LEFT)); // we are counitng how many connections we had drawn
            }
            shapeRenderer.end();

            shapeRenderer.begin(ShapeType.Filled);
            for (DComponent comp : componentList) {
                connectionRenderer.renderTraingles(shapeRenderer, comp, Gdx.input.isKeyPressed(Keys.CONTROL_LEFT));
            }
            shapeRenderer.end();

            if (renderDebug) {
                batch.setProjectionMatrix(renderDebugMatrix);
                batch.setShader(Assets.fontDistanceFieldShader);
                batch.begin();
                Assets.consolasFont.draw(batch, "FPS: " + Gdx.graphics.getFramesPerSecond(), 10,
                        Gdx.graphics.getHeight() * 10 / 7);
                Assets.consolasFont.draw(batch, "Components: " + componentList.size(), 10,
                        Gdx.graphics.getHeight() * 10 / 7 - (23 * 1));
                Assets.consolasFont.draw(batch, "Rendered components: " + renderedComponents, 10,
                        Gdx.graphics.getHeight() * 10 / 7 - (23 * 2));
                Assets.consolasFont.draw(batch, "Rendered connections: " + renderedConnections, 10,
                        Gdx.graphics.getHeight() * 10 / 7 - (23 * 3));
                batch.end();
                batch.setShader(null);
            }

            if (componentList.size() == 0) {
                batch.setProjectionMatrix(renderInfoTextMatrix);
                batch.setShader(Assets.fontDistanceFieldShader);
                batch.begin();
                infoText.draw(batch);
                batch.end();
                batch.setShader(null);
            }
        }
    }

    @Override
    public void dispose() {
        if (!disposed) {
            Assets.dispose();
            shapeRenderer.dispose();
            batch.dispose();
            disposed = true;
        }
    }

    public OrthographicCamera getCamera() {
        return camera;
    }

    @Override
    public void resize(int width, int height) {
        Vector3 pos = camera.position.cpy();
        camera.setToOrtho(false, width, height);
        camera.position.x = pos.x;
        camera.position.y = pos.y;

        renderDebugMatrix.setToOrtho2D(0, 0, width, height);
        renderDebugMatrix.scl(0.7f);

        renderInfoTextMatrix.setToOrtho2D(0, 0, width, height);

        infoText.center(Gdx.graphics.getWidth());
        infoText.setY(Gdx.graphics.getHeight() / 2);
    }

    public void addComponent(DComponentType componentType) {
        dirty = true;
        if (componentList.size() > 0) // if list is empty, we don't have any sequnce loaded(sequnce must have start component)
        {
            switch (componentType) {
            case CHOICE:
                componentList.add(new ChoiceComponent(Touch.getX(), Touch.getY()));
                break;
            case RANDOM:
                componentList.add(new RandomComponent(Touch.getX(), Touch.getY()));
                break;
            case TEXT:
                componentList.add(new TextComponent(Touch.getX(), Touch.getY()));
                break;
            case CALLBACK:
                componentList.add(new CallbackComponent(Touch.getX(), Touch.getY()));
                break;
            case CBCHECK:
                componentList.add(new CallbackCheckComponent(Touch.getX(), Touch.getY()));
                break;
            case END:
                componentList.add(new EndComponent(Touch.getX(), Touch.getY()));
                break;
            case RELAY:
                componentList.add(new RelayComponent(Touch.getX(), Touch.getY()));
                break;
            default:
                break;
            }
        } else
            listener.showMsg("Create or load project to edit dialouge structure", "Error",
                    JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Find selected connection
     * @param x in screen cords
     * @param y in screen cords
     */
    private void findConnection(float x, float y, boolean touchUp) {
        x = Touch.calcX(x);
        y = Touch.calcY(y);

        boolean found = false;
        for (DComponent comp : componentList) {
            if (comp.isVisible() == false)
                continue;

            Connector connector = comp.connectionContains(x, y);
            if (connector != null) // in drawing mode
            {
                if (selectedConnector != null && selectedConnector != connector
                        && selectedConnector.getParrentComponent() != connector.getParrentComponent() && touchUp) {
                    if (selectedConnector.isInput() != connector.isInput()) // to prevent connecting 2 outputs or 2 inputs
                    {
                        if (selectedConnector.isInput() && connector.getTarget() == null) {
                            // proper target found, adding
                            connector.addTarget(selectedConnector);
                            selectedConnector.addTarget(connector);
                            return;
                        }

                        if (selectedConnector.isInput() == false && selectedConnector.getTarget() == null) {
                            connector.addTarget(selectedConnector);
                            selectedConnector.addTarget(connector);
                            return;
                        }
                    }
                }

                selectedConnector = connector;
                found = true;
                break;
            }
        }
        if (found == false)
            selectedConnector = null;
    }

    //@formatter:off
    @Override
    public void resume() {
    }

    @Override
    public void pause() {
    }
    //@formatter:on

    public void setRenderCurves(boolean renderCurves) {
        connectionRenderer.setRenderCurves(renderCurves);
    }

    public void setRenderDebug(boolean renderDebug) {
        this.renderDebug = renderDebug;
    }

    public ArrayList<DComponent> getComponentList() {
        return componentList;
    }

    /**
     * Change componentList and perfom cleanup
     */
    public void setComponentList(ArrayList<DComponent> componentList) {
        if (dirty) {
            listener.showSaveDialog();
        }
        this.componentList = componentList;
        rectangularSelection.setComponentList(componentList); // rectangularselection potrzebuje listy do znajdywania kompponentw
        selectedComponent = null;
        selectedConnector = null;

        undoList.clear();
        redoList.clear();
    }

    // public void setProject(Project project)
    // {
    // this.project = project;
    // }

    public void undo() {
        if (undoList.size() > 0) {
            ArrayList<DComponent> undoComponents = undoList.get(undoList.size() - 1);

            redoList.add(undoComponents);

            componentList.addAll(undoComponents);

            undoList.remove(undoList.size() - 1);
        }
    }

    public void redo() {
        if (redoList.size() > 0) {
            ArrayList<DComponent> redoComponents = redoList.get(redoList.size() - 1);

            removeComponentList(redoComponents);

            redoList.remove(redoList.size() - 1);
        }
    }

    public void removeComponent(DComponent comp) {
        dirty = true;
        if (selectedComponent instanceof StartComponent) {
            listener.showMsg("This component cannot be deleted!", "Error", JOptionPane.ERROR_MESSAGE);
            return;
        }

        listener.changePropertyTableModel(null);
        comp.detachAll();

        componentList.remove(comp);

        ArrayList<DComponent> tempList = new ArrayList<DComponent>();
        tempList.add(comp);

        undoList.add(tempList);

        selectedComponent = null;
    }

    public void removeComponentList(ArrayList<DComponent> compList) {
        dirty = true;

        Iterator<DComponent> it = compList.iterator();

        while (it.hasNext()) {
            DComponent comp = it.next();
            if (comp instanceof StartComponent) {
                it.remove();
                // listener.showMsg("Selection containts components that cannot be deleted!", "Error", JOptionPane.ERROR_MESSAGE);
                // compList.clear();
                // return;
            }
        }

        for (DComponent comp : compList)
            comp.detachAllNotOnList(compList);

        componentList.removeAll(compList);

        undoList.add(new ArrayList<DComponent>(compList));

        selectedComponent = null;
        selectedComponentsList.clear();
        listener.changePropertyTableModel(null);

    }

    public void setProject(Project project) {
        this.project = project;
    }

    public boolean isDirty() {
        return dirty;
    }

    public void setDirty(boolean dirty) {
        this.dirty = dirty;
    }

    // ==================================================================INPUT============================================================================
    @Override
    public boolean scrolled(int amount) {
        float newZoom = 0;
        if (amount == 1) // out
        {
            if (camera.zoom >= 4)
                return false;

            newZoom = camera.zoom + 0.1f * camera.zoom * 2;

            camera.position.x = Touch.getX() + (camera.zoom / newZoom) * (camera.position.x - Touch.getX());
            camera.position.y = Touch.getY() + (camera.zoom / newZoom) * (camera.position.y - Touch.getY());

            camera.zoom += 0.1f * camera.zoom * 2;
        }

        if (amount == -1) // in
        {
            if (camera.zoom <= 0.5f)
                return false;

            newZoom = camera.zoom - 0.1f * camera.zoom * 2;

            camera.position.x = Touch.getX() + (newZoom / camera.zoom) * (camera.position.x - Touch.getX());
            camera.position.y = Touch.getY() + (newZoom / camera.zoom) * (camera.position.y - Touch.getY());

            camera.zoom -= 0.1f * camera.zoom * 2;
        }
        return true;
    }

    // pan is worse because you must drag mouse a little bit to fire this event
    @Override
    public boolean pan(float x, float y, float deltaX, float deltaY) {
        if (selectedComponentsList.size() > 0 && selectedConnector == null) // FIXME gdy przesuwam kompoennty w lewo, one lekko przesuwaja sie w strone srodka, dlaczego? nie zawsze tak sie dzieje...
        {
            for (DComponent comp : selectedComponentsList) {
                comp.setX((int) (comp.getX() + deltaX * camera.zoom));
                comp.setY((int) (comp.getY() - deltaY * camera.zoom));
            }
            return true;
        }

        if (panCounter > 0) //this will igore moving camera when clicked somewhere with menu opened
        {
            if (selectedComponent == null && selectedConnector == null && selectedComponentsList.size() == 0) {
                if (Gdx.input.isButtonPressed(Buttons.RIGHT)
                        && Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) == false) {
                    cameraDragged = true;
                    camera.position.x = camera.position.x - deltaX * camera.zoom;
                    camera.position.y = camera.position.y + deltaY * camera.zoom;
                    return true;
                }
            }
        } else
            panCounter++;

        return false;
    }

    @Override
    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
        if (button == Buttons.RIGHT && cameraDragged == false) {
            listener.mouseRightClicked(screenX, screenY); // ! we are sending raw coordinates
            return true;
        }

        cameraDragged = false;

        findConnection(screenX, screenY, true);
        return false;
    }

    @Override
    public boolean touchDragged(int screenX, int screenY, int pointer) {
        if (selectedComponentsList.size() > 0) {
            return false;
        }

        if (selectedComponent != null && selectedConnector == null && Gdx.input.isButtonPressed(Buttons.LEFT)) {
            selectedComponent.setX(Touch.calcX(screenX) - attachPointX);
            selectedComponent.setY(Touch.calcY(screenY) - attachPointY);
            return true;
        }
        return false;
    }

    @Override
    public boolean touchDown(float x, float y, int pointer, int button) {
        panCounter = 0;

        x = Touch.calcX(x);
        y = Touch.calcY(y);

        boolean found = false;
        for (DComponent comp : componentList) {
            if (comp.contains(x, y)) {
                dirty = true;

                if (selectedComponentsList.contains(comp)) {
                    if (selectedComponent != null)
                        return false;
                } else
                    selectedComponentsList.clear();

                selectedComponent = comp;
                attachPointX = (int) (x - selectedComponent.getX());
                attachPointY = (int) (y - selectedComponent.getY());
                found = true;
                listener.changePropertyTableModel(selectedComponent.getTableModel());

                break;
            }
        }

        if (found == false) {
            selectedComponent = null;
            listener.changePropertyTableModel(null);
            selectedComponentsList.clear();
        } else
            return true;

        return false;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean keyDown(int keycode) {
        if (keycode == Keys.FORWARD_DEL) // because forward_del is delete key, del is backspace!
        {
            if (selectedComponent != null && selectedComponentsList.size() == 0) {
                removeComponent(selectedComponent);
            }

            if (selectedComponentsList.size() > 0) {
                removeComponentList(selectedComponentsList);
            }
        }

        if (keycode == Keys.BACKSPACE && selectedComponent != null) {
            dirty = true;
            selectedComponent.detachAll();
            return true;
        }

        if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) && Gdx.input.isKeyPressed(Keys.A)) {
            dirty = true;
            selectedComponentsList.clear();
            selectedComponentsList.addAll(componentList);
        }

        if (selectedComponentsList.size() > 0 && Gdx.input.isKeyPressed(Keys.CONTROL_LEFT)
                && Gdx.input.isKeyPressed(Keys.C)) {
            dirty = true;
            clipboardList = new ArrayList<DComponent>(selectedComponentsList);
        }

        if (clipboardList != null && Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) && Gdx.input.isKeyPressed(Keys.V)) {
            dirty = true;
            // TODO better positing system
            int x = clipboardList.get(0).getX(); // we will need this to position component later
            int y = clipboardList.get(0).getY();

            // probably the best way to deep copy object is to serialize it and deserialize, we can use xstream for that
            String result = xstream.toXML(clipboardList);
            clipboardList = (ArrayList<DComponent>) xstream.fromXML(result);

            for (DComponent comp : clipboardList) // we need to asign new id's to components
            {
                if (comp.getClass() == StartComponent.class) {
                    comp.detachAll();
                    continue;
                }

                comp.detachAllNotOnList(clipboardList);
                comp.setX(Touch.getX() + (comp.getX() - x)); // move new component to cursor pos
                comp.setY(Touch.getY() + (comp.getY() - y));
            }

            componentList.addAll(clipboardList);
        }

        return false;
    }

    @Override
    public boolean mouseMoved(int screenX, int screenY) {
        findConnection(screenX, screenY, false);

        return false;
    }

    @Override
    public boolean tap(float x, float y, int count, int button) {

        return false;
    }

    @Override
    public boolean longPress(float x, float y) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean fling(float velocityX, float velocityY, int button) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean panStop(float x, float y, int pointer, int button) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean zoom(float initialDistance, float distance) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean keyUp(int keycode) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean keyTyped(char character) {
        return false;
    }

    @Override
    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
        return false;
    }
}