com.kotcrab.vis.editor.module.scene.EntityManipulatorModule.java Source code

Java tutorial

Introduction

Here is the source code for com.kotcrab.vis.editor.module.scene.EntityManipulatorModule.java

Source

/*
 * Copyright 2014-2015 See AUTHORS file.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kotcrab.vis.editor.module.scene;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.utils.DragAndDrop.Payload;
import com.badlogic.gdx.utils.Array;
import com.kotcrab.vis.editor.App;
import com.kotcrab.vis.editor.event.StatusBarEvent;
import com.kotcrab.vis.editor.module.InjectModule;
import com.kotcrab.vis.editor.module.editor.ColorPickerModule;
import com.kotcrab.vis.editor.module.editor.ObjectSupportModule;
import com.kotcrab.vis.editor.module.project.FileAccessModule;
import com.kotcrab.vis.editor.module.project.FontCacheModule;
import com.kotcrab.vis.editor.module.project.SceneIOModule;
import com.kotcrab.vis.editor.scene.EditorObject;
import com.kotcrab.vis.editor.scene.ObjectGroup;
import com.kotcrab.vis.editor.ui.scene.entityproperties.EntityProperties;
import com.kotcrab.vis.editor.util.gdx.MenuUtils;
import com.kotcrab.vis.ui.widget.PopupMenu;

public class EntityManipulatorModule extends SceneModule {
    @InjectModule
    private CameraModule camera;
    @InjectModule
    private UndoModule undoModule;
    @InjectModule
    private SceneIOModule sceneIOModule;

    private ShapeRenderer shapeRenderer;

    private EntityProperties entityProperties;

    private Array<EditorObject> entities;

    //required for setting position of pasted elements
    private float copyAttachX, copyAttachY;
    private Array<EditorObject> entitiesClipboard = new Array<>();

    private RectangularSelection rectangularSelection;
    private final Array<EditorObject> selectedEntities = new Array<>();

    private float lastTouchX, lastTouchY;

    private boolean mouseInsideSelected;
    private boolean cameraDragged;
    private boolean dragging;
    private boolean dragged;

    private Array<MoveAction> moveActions = new Array<>();

    private PopupMenu generalPopupMenu;
    private PopupMenu entityPopupMenu;
    private float menuX, menuY;

    @Override
    public void init() {
        this.entities = scene.entities;

        createPopupMenu();

        shapeRenderer = sceneContainer.get(RendererModule.class).getShapeRenderer();

        ObjectSupportModule supportManager = projectContainer.get(ObjectSupportModule.class);
        FileAccessModule fileAccess = projectContainer.get(FileAccessModule.class);
        ColorPickerModule pickerModule = container.get(ColorPickerModule.class);
        FontCacheModule fontCacheModule = projectContainer.get(FontCacheModule.class);
        entityProperties = new EntityProperties(supportManager, fileAccess, fontCacheModule, undoModule,
                pickerModule.getPicker(), sceneTab, selectedEntities);

        rectangularSelection = new RectangularSelection(entities, this);
    }

    @Override
    public void postInit() {
        entityProperties.loadSupportsSpecificTables(projectContainer.get(ObjectSupportModule.class));
    }

    @Override
    public void render(Batch batch) {
        batch.end();
        if (selectedEntities.size > 0) {

            shapeRenderer.setProjectionMatrix(camera.getCombinedMatrix());
            shapeRenderer.setColor(Color.WHITE);
            shapeRenderer.begin(ShapeType.Line);

            for (EditorObject entity : selectedEntities) {
                Rectangle bounds = entity.getBoundingRectangle();
                shapeRenderer.rect(bounds.x, bounds.y, bounds.width, bounds.height);
            }

            shapeRenderer.end();

        }

        rectangularSelection.render(shapeRenderer);

        batch.begin();
    }

    @Override
    public void dispose() {
        entityProperties.dispose();

        for (EditorObject entity : entities)
            entity.dispose();
    }

    private void createPopupMenu() {
        entityPopupMenu = new PopupMenu();
        entityPopupMenu.addItem(MenuUtils.createMenuItem("Cut", this::cut));
        entityPopupMenu.addItem(MenuUtils.createMenuItem("Copy", this::copy));
        entityPopupMenu.addItem(MenuUtils.createMenuItem("Paste", this::paste));
        entityPopupMenu.addItem(MenuUtils.createMenuItem("Remove", this::deleteSelectedEntities));
        entityPopupMenu.addItem(MenuUtils.createMenuItem("Select All", this::selectAll));

        generalPopupMenu = new PopupMenu();
        generalPopupMenu.addItem(MenuUtils.createMenuItem("Paste", this::paste));
        generalPopupMenu.addItem(MenuUtils.createMenuItem("Select All", this::selectAll));
    }

    private void selectedEntitiesToClipboard() {
        if (selectedEntities.size > 0) {
            entitiesClipboard.clear();
            entitiesClipboard.addAll(sceneIOModule.getKryo().copy(selectedEntities));

            EditorObject lastEntity = selectedEntities.peek();

            if (entityPopupMenu.getParent() != null) { //is menu visible
                copyAttachX = menuX - lastEntity.getX();
                copyAttachY = menuY - lastEntity.getY();
            } else {
                copyAttachX = lastEntity.getWidth() / 2;
                copyAttachY = lastEntity.getHeight() / 2;
            }
        }
    }

    private void cut() {
        selectedEntitiesToClipboard();
        deleteSelectedEntities();
        sceneTab.dirty();
    }

    private void copy() {
        selectedEntitiesToClipboard();
    }

    private void paste() {
        if (entitiesClipboard.size > 0) {
            float x = camera.getInputX();
            float y = camera.getInputY();

            EditorObject baseEntity = entitiesClipboard.peek();
            float xOffset = baseEntity.getX();
            float yOffset = baseEntity.getY();

            for (EditorObject entity : entitiesClipboard) {
                float px = x - copyAttachX + (entity.getX() - xOffset);
                float py = y - copyAttachY + (entity.getY() - yOffset);

                entity.setPosition(px, py);
            }

            undoModule.execute(new EntitiesAddedAction(entitiesClipboard));

            Array<EditorObject> newClipboard = sceneIOModule.getKryo().copy(entitiesClipboard);
            entitiesClipboard.clear();
            entitiesClipboard.addAll(newClipboard);
        } else
            App.eventBus.post(new StatusBarEvent("Clipboard is empty, nothing to paste!"));
    }

    public EntityProperties getEntityProperties() {
        return entityProperties;
    }

    private boolean isMouseInsideSelectedEntities(float x, float y) {
        for (EditorObject entity : selectedEntities) {
            if (entity.getBoundingRectangle().contains(x, y)) {
                EditorObject result = findEntityWithSmallestSurfaceArea(x, y);
                if (result == entity)
                    return true;
            }
        }

        return false;
    }

    private boolean isMouseInsideEntities(float x, float y) {
        for (EditorObject obj : entities) {
            if (obj.getBoundingRectangle().contains(x, y))
                return true;
        }

        return false;
    }

    public void processDropPayload(Payload payload) {
        Object obj = payload.getObject();

        if (obj instanceof EditorObject) {
            EditorObject entity = (EditorObject) obj;
            float x = camera.getInputX() - entity.getWidth() / 2;
            float y = camera.getInputY() - entity.getHeight() / 2;
            entity.setPosition(x, y);

            undoModule.execute(new EntityAddedAction(entity));
        }
    }

    @Override
    public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
        x = camera.getInputX();
        y = camera.getInputY();

        if (button == Buttons.LEFT) {
            dragging = true;
            lastTouchX = x;
            lastTouchY = y;

            if (isMouseInsideEntities(x, y) == false) {
                rectangularSelection.touchDown(x, y, button);
                return true;
            }

            if (isMouseInsideSelectedEntities(x, y) == false) {
                //multiple select made easy
                if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) == false)
                    selectedEntities.clear();

                EditorObject result = findEntityWithSmallestSurfaceArea(x, y);
                if (result != null && selectedEntities.contains(result, true) == false)
                    selectedEntities.add(result);

                entityProperties.selectedEntitiesChanged();
                mouseInsideSelected = true;
                return true;
            }
        }

        return false;
    }

    @Override
    public void touchDragged(InputEvent event, float x, float y, int pointer) {
        if (Gdx.input.isButtonPressed(Buttons.LEFT)) {
            x = camera.getInputX();
            y = camera.getInputY();

            if (dragged == false) {
                moveActions.clear();
                for (EditorObject entity : selectedEntities)
                    moveActions.add(new MoveAction(entity));
            }

            if (rectangularSelection.touchDragged(x, y) == false) {

                if (dragging && selectedEntities.size > 0) {
                    dragged = true;
                    float deltaX = (x - lastTouchX);
                    float deltaY = (y - lastTouchY);

                    for (EditorObject entity : selectedEntities)
                        entity.setPosition(entity.getX() + deltaX, entity.getY() + deltaY);

                    lastTouchX = x;
                    lastTouchY = y;

                    sceneTab.dirty();
                    entityProperties.updateValues();
                }

            }
        }

        if (Gdx.input.isButtonPressed(Buttons.RIGHT))
            cameraDragged = true;
    }

    @Override
    public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
        x = camera.getInputX();
        y = camera.getInputY();

        if (button == Buttons.LEFT && dragged == false && mouseInsideSelected == false) {
            EditorObject result = findEntityWithSmallestSurfaceArea(x, y);
            if (result != null)
                selectedEntities.removeValue(result, true);

            entityProperties.selectedEntitiesChanged();
        }

        if (button == Buttons.RIGHT && cameraDragged == false) {
            if (isMouseInsideSelectedEntities(x, y) == false)
                if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) == false)
                    selectedEntities.clear();

            EditorObject result = findEntityWithSmallestSurfaceArea(x, y);
            if (result != null && selectedEntities.contains(result, true) == false)
                selectedEntities.add(result);

            entityProperties.selectedEntitiesChanged();
            mouseInsideSelected = true;

            if (selectedEntities.size > 0) {
                menuX = x;
                menuY = y;

                entityPopupMenu.showMenu(event.getStage(), event.getStageX(), event.getStageY());
            } else
                generalPopupMenu.showMenu(event.getStage(), event.getStageX(), event.getStageY());
        }

        if (dragged) {
            for (int i = 0; i < selectedEntities.size; i++)
                moveActions.get(i).newData.saveFrom(selectedEntities.get(i));

            UndoableActionGroup group = new UndoableActionGroup();

            for (MoveAction action : moveActions)
                group.add(action);

            group.finalizeGroup();

            undoModule.add(group);
        }

        rectangularSelection.touchUp();

        lastTouchX = 0;
        lastTouchY = 0;
        mouseInsideSelected = false;
        dragging = false;
        dragged = false;
        cameraDragged = false;
    }

    @Override
    public boolean keyDown(InputEvent event, int keycode) {
        if (keycode == Keys.FORWARD_DEL) { //Delete
            deleteSelectedEntities();
            return true;
        }

        if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) && keycode == Keys.A)
            selectAll();
        if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) && keycode == Keys.C)
            copy();
        if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) && keycode == Keys.V)
            paste();
        if (Gdx.input.isKeyPressed(Keys.CONTROL_LEFT) && keycode == Keys.X)
            cut();

        return false;
    }

    private void deleteSelectedEntities() {
        undoModule.execute(new EntityRemovedAction(selectedEntities));
    }

    /**
     * Returns entity with smallest surface area that contains point x,y.
     * <p>
     * When selecting entities, and few of them are overlapping, selecting entity with smallest
     * area gives better results than just selecting first one.
     */
    private EditorObject findEntityWithSmallestSurfaceArea(float x, float y) {
        EditorObject matchingEntity = null;
        float lastSurfaceArea = Float.MAX_VALUE;

        for (EditorObject entity : entities) {
            Rectangle entityBoundingRectangle = entity.getBoundingRectangle();
            if (entityBoundingRectangle.contains(x, y)) {

                float currentSurfaceArea = entityBoundingRectangle.width * entityBoundingRectangle.height;

                if (currentSurfaceArea < lastSurfaceArea) {
                    matchingEntity = entity;
                    lastSurfaceArea = currentSurfaceArea;
                }
            }
        }

        return matchingEntity;
    }

    public int getEntityCount() {
        return entities.size;
    }

    public Array<EditorObject> getSelectedEntities() {
        return selectedEntities;
    }

    public void select(EditorObject entity) {
        if (entities.contains(entity, true) == false)
            throw new IllegalArgumentException("Cannot select entity that isn't added to entity list");

        selectedEntities.clear();
        selectedEntities.add(entity);
        entityProperties.selectedEntitiesChanged();
    }

    void selectAppend(EditorObject entity) {
        if (entities.contains(entity, true) == false)
            throw new IllegalArgumentException("Cannot select entity that isn't added to entity list");

        if (selectedEntities.contains(entity, true) == false)
            selectedEntities.add(entity);

        entityProperties.selectedEntitiesChanged();

    }

    private void selectAll() {
        for (EditorObject entity : entities) {
            if (selectedEntities.contains(entity, true) == false)
                selectedEntities.add(entity);
        }

        entityProperties.selectedEntitiesChanged();
    }

    public void resetSelection() {
        selectedEntities.clear();
        entityProperties.selectedEntitiesChanged();
    }

    public void groupSelection() {
        if (selectedEntities.size == 0) {
            App.eventBus.post(new StatusBarEvent("Nothing to group!"));
            return;
        }

        UndoableActionGroup actionGroup = new UndoableActionGroup();

        ObjectGroup objGroup = new ObjectGroup();
        objGroup.addEntities(selectedEntities);

        actionGroup.execute(new EntityRemovedAction(selectedEntities));
        actionGroup.execute(new EntityAddedAction(objGroup));
        actionGroup.finalizeGroup();

        undoModule.add(actionGroup);
    }

    public void ungroupSelection() {
        if (selectedEntities.size == 0) {
            App.eventBus.post(new StatusBarEvent("Nothing to ungroup!"));
            return;
        }

        UndoableActionGroup actionGroup = new UndoableActionGroup();

        for (EditorObject obj : selectedEntities) {
            if (obj instanceof ObjectGroup) {
                ObjectGroup group = (ObjectGroup) obj;

                actionGroup.add(new EntityRemovedAction(group));
                actionGroup.add(new EntitiesAddedAction(group.getObjects()));
            }
        }

        actionGroup.finalizeGroup();

        undoModule.execute(actionGroup);
    }

    private class EntitiesAddedAction implements UndoableAction {
        private Array<EditorObject> newEntities;

        public EntitiesAddedAction(Array<EditorObject> newEntities) {
            this.newEntities = new Array<>(newEntities);
        }

        @Override
        public void execute() {
            entities.addAll(newEntities);
            resetSelection();
            newEntities.forEach(EntityManipulatorModule.this::selectAppend);
            sceneTab.dirty();
        }

        @Override
        public void undo() {
            scene.entities.removeAll(newEntities, true);
            resetSelection();
            sceneTab.dirty();
        }
    }

    private class EntityAddedAction implements UndoableAction {
        private EditorObject entity;

        public EntityAddedAction(EditorObject entity) {
            this.entity = entity;
        }

        @Override
        public void execute() {
            scene.entities.add(entity);
            select(entity);
            sceneTab.dirty();
        }

        @Override
        public void undo() {
            scene.entities.removeValue(entity, true);
            resetSelection();
            sceneTab.dirty();
        }
    }

    private class EntityRemovedAction implements UndoableAction {
        private Array<Integer> indexes;
        private Array<EditorObject> entities;

        public EntityRemovedAction(Array<EditorObject> selectedEntities) {
            indexes = new Array<>(selectedEntities.size);
            entities = new Array<>(selectedEntities);
        }

        public EntityRemovedAction(EditorObject object) {
            indexes = new Array<>(1);
            entities = new Array<>(1);
            entities.add(object);
        }

        @Override
        public void execute() {
            for (EditorObject entity : entities)
                indexes.add(scene.entities.indexOf(entity, true));

            scene.entities.removeAll(entities, true);

            resetSelection();
        }

        @Override
        public void undo() {
            for (int i = 0; i < entities.size; i++) {
                int index = indexes.get(i);
                EditorObject obj = entities.get(i);

                if (index > scene.entities.size)
                    scene.entities.add(obj);
                else
                    scene.entities.insert(index, obj);
            }

            resetSelection();
        }
    }

    private class MoveAction implements UndoableAction {
        private EntityPositionData oldData = new EntityPositionData();
        private EntityPositionData newData = new EntityPositionData();
        private EditorObject entity;

        public MoveAction(EditorObject entity) {
            this.entity = entity;
            oldData.saveFrom(entity);
        }

        @Override
        public void execute() {
            newData.loadTo(entity);
        }

        @Override
        public void undo() {
            oldData.loadTo(entity);
        }
    }

    private class EntityPositionData {
        public float x;
        public float y;

        public void saveFrom(EditorObject entity) {
            x = entity.getX();
            y = entity.getY();
        }

        public void loadTo(EditorObject entity) {
            entity.setPosition(x, y);
        }
    }
}