view.EditorView.java Source code

Java tutorial

Introduction

Here is the source code for view.EditorView.java

Source

package view;

/*-
 * #%L
 * Zork Clone
 * %%
 * Copyright (C) 2016 Frederik Kammel
 * %%
 * 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.
 * #L%
 */

import com.github.vatbub.common.core.Common;
import com.github.vatbub.common.core.logging.FOKLogger;
import com.github.vatbub.common.view.core.CustomGroup;
import com.github.vatbub.common.view.core.ExceptionAlert;
import com.github.vatbub.common.view.reporting.ReportingDialog;
import common.AppConfig;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ZoomEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Line;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import model.Game;
import model.Room;
import model.WalkDirection;
import model.WalkDirectionUtils;
import org.apache.commons.lang.exception.ExceptionUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;

public class EditorView extends Application {

    public static EditorView currentEditorInstance;
    public static ResourceBundle bundle;
    private static Stage stage;
    public final ConnectionLineList lineList = new ConnectionLineList();
    private final ObjectProperty<Game> currentGame = new SimpleObjectProperty<>();
    private boolean unselectingDisabled;
    // Unconnected Rooms will not be saved but need to be hold in the RAM while editing
    private RoomRectangleList unconnectedRooms = new RoomRectangleList();
    private RoomRectangleList allRoomsAsList;
    private RoomRectangleList allRoomsAsListCopy;
    private EditMode currentEditMode = EditMode.MOVE;
    private EditMode previousEditMode;
    private boolean isMouseOverDrawing = false;
    private boolean compassIconFaded = false;
    private boolean insertRoomDragDetected;
    private ExecutorService renderThreadPool = Executors.newFixedThreadPool(1);

    /**
     * Used to display a temporary room when in EditMode.INSERT_ROOM
     */
    private RoomRectangle tempRoomForRoomInsertion;
    /**
     * A thread safe room counter
     */
    private int currentRoomCount = 0;
    private double currentMouseX = 0;
    private double currentMouseY = 0;
    @SuppressWarnings("unused")
    @FXML // ResourceBundle that was given to the FXMLLoader
    private ResourceBundle resources;
    @SuppressWarnings("unused")
    @FXML // URL location of the FXML file that was given to the FXMLLoader
    private URL location;
    @FXML // fx:id="insertRoom"
    private ToggleButton insertRoom; // Value injected by FXMLLoader
    @FXML // fx:id="drawing"
    private CustomGroup drawing; // Value injected by FXMLLoader
    private final ConnectionLine.InvalidationRunnable lineInvalidationRunnable = (lineToDispose) -> {
        FOKLogger.info(EditorView.class.getName(),
                "Invalidated line that connected " + lineToDispose.getStartRoom().getRoom().getName() + " and "
                        + lineToDispose.getEndRoom().getRoom().getName());
        lineList.remove(lineToDispose);
        if (lineToDispose.getStartRoom().getRoom().isDirectlyConnectedTo(lineToDispose.getEndRoom().getRoom())) {
            // Connection between rooms must be deleted
            lineToDispose.getStartRoom().getRoom().getAdjacentRooms()
                    .remove(WalkDirectionUtils.getFromLine(lineToDispose));
            lineToDispose.getEndRoom().getRoom().getAdjacentRooms()
                    .remove(WalkDirectionUtils.invert(WalkDirectionUtils.getFromLine(lineToDispose)));
        }
        Platform.runLater(() -> drawing.getChildren().remove(lineToDispose));
    };
    @FXML // fx:id="insertPath"
    private ToggleButton insertPath; // Value injected by FXMLLoader
    @FXML
    private ToggleButton moveButton;
    @FXML
    private Button autoLayoutButton;
    @FXML
    private Button refreshViewButton;
    @SuppressWarnings("unused")
    @FXML
    private MenuItem newMenuItem;
    @FXML
    private ScrollPane scrollPane;
    @FXML
    private ImageView compassImage;
    @SuppressWarnings("unused")
    @FXML
    private MenuItem menuItemClose;
    @SuppressWarnings("unused")
    @FXML
    private MenuItem menuItemOpen;
    @FXML
    private MenuItem menuItemSave;
    @SuppressWarnings("unused")
    @FXML
    private MenuItem menuItemSaveAs;
    @FXML
    private AnchorPane scrollPaneContainer;
    @SuppressWarnings("unused")
    private EventHandler<MouseEvent> forwardEventsToSelectableNodesHandler = (event -> {
        if (getCurrentEditMode() == EditMode.MOVE) {
            for (Node child : new ArrayList<>(drawing.getChildren())) {
                if (child instanceof Selectable) {
                    if (((Selectable) child).isSelected() && event.getTarget() != child) {
                        FOKLogger.fine(EditorView.class.getName(),
                                "Child is:  " + child.toString() + "\ntarget is: " + event.getTarget().toString());
                        child.fireEvent(event);
                        event.consume();
                    }
                }
            }
        }
    });

    public static void main(String[] args) {
        Common.setAppName("zorkGameEditor");
        Common.setAwsAccessKey(AppConfig.awsLogAccessKeyID);
        Common.setAwsSecretAccessKey(AppConfig.awsLogSecretAccessKeyID);
        FOKLogger.enableLoggingOfUncaughtExceptions();
        for (String arg : args) {
            if (arg.toLowerCase().matches("mockappversion=.*")) {
                // Set the mock version
                String version = arg.substring(arg.toLowerCase().indexOf('=') + 1);
                Common.setMockAppVersion(version);
            } else if (arg.toLowerCase().matches("mockbuildnumber=.*")) {
                // Set the mock build number
                String buildnumber = arg.substring(arg.toLowerCase().indexOf('=') + 1);
                Common.setMockBuildNumber(buildnumber);
            } else if (arg.toLowerCase().matches("mockpackaging=.*")) {
                // Set the mock packaging
                String packaging = arg.substring(arg.toLowerCase().indexOf('=') + 1);
                Common.setMockPackaging(packaging);
            } else if (arg.toLowerCase().matches("locale=.*")) {
                // set the gui language
                String guiLanguageCode = arg.substring(arg.toLowerCase().indexOf('=') + 1);
                FOKLogger.info(MainWindow.class.getName(), "Setting language: " + guiLanguageCode);
                Locale.setDefault(new Locale(guiLanguageCode));
            }
        }

        launch(args);
    }

    @FXML
    void fileBugMenuItemOnAction(@SuppressWarnings("unused") ActionEvent event) {
        FOKLogger.info(EditorView.class.getName(), "Manual call of the ReportingDialog");
        new ReportingDialog(stage.getScene()).show(AppConfig.gitHubUserName, AppConfig.gitHubRepoName);
    }

    @FXML
    void menuItemSaveOnAction(ActionEvent event) {
        if (getCurrentGame().getFileSource() == null) {
            // show the save as dialog
            menuItemSaveAsOnAction(event);
        } else {
            // simply save
            try {
                getCurrentGame().save(getCurrentGame().getFileSource());
            } catch (IOException e) {
                FOKLogger.log(MainWindow.class.getName(), Level.SEVERE,
                        "Could not save the game from the \"Save\" menu", e);
                new Alert(Alert.AlertType.ERROR,
                        "Could not save the game file: \n\n" + ExceptionUtils.getRootCauseMessage(e)).show();
            }
        }
    }

    @FXML
    void menuItemSaveAsOnAction(@SuppressWarnings("unused") ActionEvent event) {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Save game file");
        File file = fileChooser.showSaveDialog(stage);

        if (file != null) {
            // if file == null the action was aborted
            try {
                getCurrentGame().save(file);
            } catch (IOException e) {
                FOKLogger.log(MainWindow.class.getName(), Level.SEVERE,
                        "Could not save the game from the \"Save As\" menu", e);
                new Alert(Alert.AlertType.ERROR,
                        "Could not save the game file: \n\n" + ExceptionUtils.getRootCauseMessage(e)).show();
            }
        }
    }

    @FXML
    void menuItemOpenOnAction(@SuppressWarnings("unused") ActionEvent event) {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Open a game file");
        File file = fileChooser.showOpenDialog(stage);
        if (file != null) {
            // if file == null the action was aborted
            try {
                loadGame(file);
            } catch (IOException | ClassNotFoundException e) {
                FOKLogger.log(MainWindow.class.getName(), Level.SEVERE, "Failed to open game " + file.toString(),
                        e);
                new Alert(Alert.AlertType.ERROR,
                        "Could not open the game file: \n\n" + ExceptionUtils.getRootCauseMessage(e)).show();
            }
        }
    }

    @FXML
    void menuItemCloseOnAction(@SuppressWarnings("unused") ActionEvent event) {
        Platform.exit();
    }

    @FXML
    void insertRoomOnAction(@SuppressWarnings("unused") ActionEvent event) {
        this.setCurrentEditMode(EditMode.INSERT_ROOM);
    }

    @FXML
    void insertRoomOnDragDetected(@SuppressWarnings("unused") MouseEvent event) {
        insertRoomDragDetected = true;
        setCurrentEditMode(EditMode.INSERT_ROOM);
    }

    @FXML
    void insertRoomOnMouseDragged(MouseEvent event) {
        Bounds scrollPaneBounds = scrollPaneContainer.localToScene(scrollPane.getBoundsInLocal());
        if (event.getSceneX() >= scrollPaneBounds.getMinX() && event.getSceneX() <= scrollPaneBounds.getMaxX()
                && event.getSceneY() >= scrollPaneBounds.getMinY()
                && event.getSceneY() <= scrollPaneBounds.getMaxY()) {
            // we're in the scroll pane
            if (!isMouseOverDrawing) {
                scrollPaneOnMouseEntered(event);
            }
        } else {
            if (isMouseOverDrawing) {
                scrollPaneOnMouseExited(event);
            }
        }
        scrollPaneOnMouseMoved(event);
    }

    @FXML
    void insertRoomOnMousePressed(@SuppressWarnings("unused") MouseEvent event) {
        insertRoom.setCursor(Cursor.CLOSED_HAND);
    }

    @FXML
    void insertRoomOnMouseReleased(MouseEvent event) {
        insertRoom.setCursor(Cursor.OPEN_HAND);
        if (insertRoomDragDetected) {
            insertRoomDragDetected = false;
            scrollPaneOnMouseClicked(event);
        }
    }

    @FXML
    void insertPathOnAction(@SuppressWarnings("unused") ActionEvent event) {
        this.setCurrentEditMode(EditMode.INSERT_PATH);
    }

    @FXML
    void newMenuItemOnAction(@SuppressWarnings("unused") ActionEvent event) {
        initGame();
    }

    @FXML
    void moveButtonOnAction(@SuppressWarnings("unused") ActionEvent event) {
        this.setCurrentEditMode(EditMode.MOVE);
    }

    @FXML
    void autoLayoutButtonOnAction(@SuppressWarnings("unused") ActionEvent event) {
        renderView();
    }

    @FXML
    void refreshViewButtonOnAction(@SuppressWarnings("unused") ActionEvent event) {
        renderView(false);
    }

    @FXML
    void scrollPaneOnZoom(ZoomEvent event) {
        FOKLogger.fine(MainWindow.class.getName(), "Zooming in view, new Zoom level: " + event.getZoomFactor());
        drawing.setScaleX(drawing.getScaleX() * event.getZoomFactor());
        drawing.setScaleY(drawing.getScaleY() * event.getZoomFactor());
        // TODO: Update the actual size in the scrollpane (so that scrollbars appear when zooming in
        // TODO: Add Keyboard and touchpad zoom
        // TODO: do the zoom with the right zoom center
    }

    @FXML
    void scrollPaneOnMouseReleased(MouseEvent event) {
        if (!event.isControlDown() & !unselectingDisabled) {
            FOKLogger.finest(MainWindow.class.getName(), "Unselected all rooms through clicking the scroll pane");
            unselectEverything();
        }

        unselectingDisabled = false;
    }

    @FXML
    void scrollPaneOnMouseClicked(MouseEvent event) {
        /*
        event.getTarget() instanceof RoomRectangle is necessary because the event is required twice:
        - once with event.getTarget() instanceof RoomRectangle and
        - once with event.getTarget() instanceof Label (the name label of the room)
        ... and we need to suppress one of the two because we don't want to insert two rooms
         */
        /*
        When the user uses a touch screen and adds a new room by clicking the insertRoom-button and then clicking the
        scrollPane, the event target is an instance of ScrollPaneSkin$4 which is an anonymous inner class in ScrollPane.
        Since we cannot directly check the class type using instanceof against that inner class, we need to use
        event.getTarget().getClass().getName() and do a String comparison for this particular use case.
        Keep in mind that this is an internal api of java and the class name of that class might change at ANY TIME so
        things might break just by upgrading the java version! (But we have no other choice unfortunately :( )
        See https://github.com/vatbub/zorkClone/issues/7 and http://stackoverflow.com/questions/41454202/javafx-instanceof-scrollpaneskin-fails
        for more info
         */
        FOKLogger.finest(EditorView.class.getName(), "scrollPaneOnMouseClicked occurred. event target class is "
                + event.getTarget().getClass().getName());
        if (currentEditMode == EditMode.INSERT_ROOM
                && (event.getTarget() instanceof RoomRectangle || event.getTarget() instanceof ToggleButton
                        || event.getTarget().getClass().getName()
                                .equals("com.sun.javafx.scene.control.skin.ScrollPaneSkin$4"))
                && event.getClickCount() == 1 && tempRoomForRoomInsertion != null) {
            // add tempRoomForRoomInsertion to the game
            FOKLogger.fine(MainWindow.class.getName(),
                    "Added room to game: " + tempRoomForRoomInsertion.getRoom().getName());
            tempRoomForRoomInsertion.setTemporary(false);
            tempRoomForRoomInsertion.setSelected(false);
            allRoomsAsList.add(tempRoomForRoomInsertion);
            // this.renderView(false, false, true);
            this.renderView(false, false);

            this.setCurrentEditMode(this.getPreviousEditMode());
        }
    }

    @FXML
    void scrollPaneOnMouseEntered(MouseEvent event) {
        isMouseOverDrawing = true;
        currentMouseX = event.getScreenX();
        currentMouseY = event.getScreenY();
        if (this.getCurrentEditMode() == EditMode.INSERT_ROOM) {
            initInsertRoomEditMode();
        }
    }

    @FXML
    void scrollPaneOnMouseExited(@SuppressWarnings("unused") MouseEvent event) {
        isMouseOverDrawing = false;
        if (this.getCurrentEditMode() == EditMode.INSERT_ROOM) {
            terminateInsertRoomEditMode();
        }
    }

    @FXML
    void scrollPaneOnMouseMoved(MouseEvent event) {
        currentMouseX = event.getScreenX();
        currentMouseY = event.getScreenY();
        if (currentEditMode == EditMode.INSERT_ROOM) {
            insertRoomUpdateTempRoomPosition();
        }

        // Fade compassIcon
        if (event.getX() >= compassImage.getLayoutX()
                && event.getX() <= (compassImage.getLayoutX() + compassImage.getFitWidth())
                && event.getY() >= compassImage.getLayoutY()
                && event.getY() <= (compassImage.getLayoutY() + compassImage.getFitHeight()) && !compassIconFaded) {
            // fade out
            compassIconFaded = true;
            compassImageFadeOut();
        }
        if ((event.getX() < compassImage.getLayoutX()
                || event.getX() > (compassImage.getLayoutX() + compassImage.getFitWidth())
                || event.getY() < compassImage.getLayoutY()
                || event.getY() > (compassImage.getLayoutY() + compassImage.getFitHeight())) && compassIconFaded) {
            // fade in
            compassIconFaded = false;
            compassImageFadeIn();
        }
    }

    /**
     * Performs initializing actions for the EditMode.INSERT_ROOM
     */
    private void initInsertRoomEditMode() {
        tempRoomForRoomInsertion = new RoomRectangle(null);

        int roomIndex;

        if (allRoomsAsList == null) {
            roomIndex = 0;
        } else {
            roomIndex = currentRoomCount;
        }

        tempRoomForRoomInsertion.getRoom().setName("Room " + roomIndex);
        tempRoomForRoomInsertion.setTemporary(true);
        tempRoomForRoomInsertion.setCustomParent(drawing);
        insertRoomUpdateTempRoomPosition();
    }

    /**
     * Performs terminating actions for the EditMode.INSERT_ROOM
     */
    private void terminateInsertRoomEditMode() {
        if (tempRoomForRoomInsertion != null) {
            tempRoomForRoomInsertion.setCustomParent(null);
            tempRoomForRoomInsertion = null;
        }
    }

    /**
     * Updates the position of tempRoomForRoomInsertion when the current edit mode is EditMode.INSERT_ROOM
     */
    private void insertRoomUpdateTempRoomPosition() {
        if (tempRoomForRoomInsertion != null) {
            // convert coordinates
            Point2D point = drawing.screenToLocal(currentMouseX - tempRoomForRoomInsertion.getWidth() / 2.0,
                    currentMouseY - tempRoomForRoomInsertion.getHeight() / 2.0);
            tempRoomForRoomInsertion.setX(point.getX());
            tempRoomForRoomInsertion.setY(point.getY());
        }
    }

    private void compassImageFadeOut() {
        FadeTransition ft = new FadeTransition(Duration.millis(250), compassImage);
        ft.setFromValue(compassImage.getOpacity());
        ft.setToValue(0.2);
        ft.setAutoReverse(false);
        ft.play();
    }

    private void compassImageFadeIn() {
        FadeTransition ft = new FadeTransition(Duration.millis(250), compassImage);
        ft.setFromValue(compassImage.getOpacity());
        ft.setAutoReverse(false);
        ft.setToValue(0.5);
        ft.play();
    }

    public void unselectEverything() {
        for (Node child : drawing.getChildren()) {
            if (child instanceof RoomRectangle) {
                ((RoomRectangle) child).setSelected(false);
            } else if (child instanceof ConnectionLine) {
                ((ConnectionLine) child).setSelected(false);
            }
        }
    }

    /**
     * Updates the internal connection status of a room (if it is connected to the current room of the game)
     *
     * @param room The room to update
     */
    private void updateConnectionStatusOfRoom(RoomRectangle room) {
        boolean isConnected = getCurrentGame().getCurrentRoom().isConnectedTo(room.getRoom());
        if (isConnected && unconnectedRooms.contains(room)) {
            // room was marked as unconnected and now is connected
            unconnectedRooms.remove(room);
        } else if (!isConnected && !unconnectedRooms.contains(room)) {
            // room was marked as connected and now is unconnected
            unconnectedRooms.add(room);
        }

        // in any other case, the status did not change and we don't need to do anything
    }

    /**
     * Renders the current game and unconnected rooms in the view.
     */
    public void renderView() {
        this.renderView(true);
    }

    /**
     * Renders the current game and unconnected rooms in the view.
     *
     * @param autoLayout If {@code true}, the rooms will be automatically laid out according to their topology.
     */
    public void renderView(boolean autoLayout) {
        this.renderView(autoLayout, false);
    }

    /**
     * Renders the current game and unconnected rooms in the view.
     *
     * @param autoLayout      If {@code true}, the rooms will be automatically laid out according to their topology.
     * @param onlyUpdateLines If {@code true}, only connecting lines between the rooms are rendered, rooms are left as they are. Useful if the user is currently moving the room around with the mouse.
     */
    public void renderView(boolean autoLayout, boolean onlyUpdateLines) {
        int indexCorrection = 0;
        while (drawing.getChildren().size() > indexCorrection) {
            if (!onlyUpdateLines && !(drawing.getChildren().get(indexCorrection) instanceof ConnectionLine)) {
                drawing.getChildren().remove(indexCorrection);
            } else if (drawing.getChildren().get(indexCorrection) instanceof ConnectionLine) {
                // Check if line is still valid
                ((ConnectionLine) drawing.getChildren().get(indexCorrection)).updateLocation();
                indexCorrection++;
            } else {
                indexCorrection++;
            }
        }

        renderThreadPool.submit(() -> {
            // update the connection status of all rooms
            if (allRoomsAsList != null) {
                for (RoomRectangle room : allRoomsAsList) {
                    updateConnectionStatusOfRoom(room);
                }
            }

            LinkedList<RoomRectangle> renderQueue = new LinkedList<>();

            // The distance between connected rooms
            double roomDistance = 50;

            RoomRectangle startRoom;
            if (allRoomsAsList == null) {
                // First time to render
                startRoom = new RoomRectangle(drawing, this.getCurrentGame().getCurrentRoom());
                allRoomsAsListCopy = new RoomRectangleList();
                allRoomsAsList = new RoomRectangleList();
                allRoomsAsList.add(startRoom);
            } else {
                startRoom = allRoomsAsList.findByRoom(this.getCurrentGame().getCurrentRoom());
                allRoomsAsListCopy = allRoomsAsList;
                if (!onlyUpdateLines) {
                    allRoomsAsList = new RoomRectangleList();
                }
            }

            renderQueue.add(startRoom);

            // render unconnected rooms
            renderQueue.addAll(unconnectedRooms);

            while (!renderQueue.isEmpty()) {
                RoomRectangle currentRoom = renderQueue.remove();
                if (currentRoom == null) {
                    FOKLogger.severe(EditorView.class.getName(),
                            "currentRoom == null means that the room was never added to allRoomsAsList and that means that we ran into a bug, so report it :(");
                    Platform.runLater(() -> new ReportingDialog(stage.getScene()).show(AppConfig.gitHubUserName,
                            AppConfig.gitHubRepoName, new IllegalStateException(
                                    "A room of the game was never added to allRoomsAsList. This is an internal bug and needs to be reported to the dev team. Please tell us at https://github.com/vatbub/zorkClone/issues what you did when this exception occurred.")));
                }

                //noinspection ConstantConditions
                if (!currentRoom.isRendered()) {
                    if (!allRoomsAsList.contains(currentRoom)) {
                        allRoomsAsList.add(currentRoom);
                    }
                    currentRoom.setCustomParent(drawing);
                    currentRoom.updateNameLabelPosition();
                }
                for (Map.Entry<WalkDirection, Room> entry : currentRoom.getRoom().getAdjacentRooms().entrySet()) {
                    RoomRectangle newRoom;
                    newRoom = allRoomsAsListCopy.findByRoom(entry.getValue());

                    if (newRoom == null) {
                        // not rendered yet
                        newRoom = new RoomRectangle(drawing, entry.getValue());
                        allRoomsAsList.add(newRoom);
                    }

                    // Set room position
                    if (autoLayout && !newRoom.isRendered()) {
                        switch (entry.getKey()) {
                        case NORTH:
                            newRoom.setY(currentRoom.getY() - newRoom.getHeight() - roomDistance);
                            newRoom.setX(currentRoom.getX() + currentRoom.getWidth() / 2 - newRoom.getWidth() / 2);
                            break;
                        case WEST:
                            newRoom.setY(currentRoom.getY());
                            newRoom.setX(currentRoom.getX() - newRoom.getWidth() - roomDistance);
                            break;
                        case EAST:
                            newRoom.setY(currentRoom.getY());
                            newRoom.setX(currentRoom.getX() + currentRoom.getWidth() + roomDistance);
                            break;
                        case SOUTH:
                            newRoom.setY(currentRoom.getY() + currentRoom.getHeight() + roomDistance);
                            newRoom.setX(currentRoom.getX() + currentRoom.getWidth() / 2 - newRoom.getWidth() / 2);
                            break;
                        case NORTH_WEST:
                            newRoom.setY(currentRoom.getY() - newRoom.getHeight() - roomDistance);
                            newRoom.setX(currentRoom.getX() - newRoom.getWidth() - roomDistance);
                            break;
                        case NORTH_EAST:
                            newRoom.setY(currentRoom.getY() - newRoom.getHeight() - roomDistance);
                            newRoom.setX(currentRoom.getX() + currentRoom.getWidth() + roomDistance);
                            break;
                        case SOUTH_WEST:
                            newRoom.setY(currentRoom.getY() + currentRoom.getHeight() + roomDistance);
                            newRoom.setX(currentRoom.getX() - newRoom.getWidth() - roomDistance);
                            break;
                        case SOUTH_EAST:
                            newRoom.setY(currentRoom.getY() + currentRoom.getHeight() + roomDistance);
                            newRoom.setX(currentRoom.getX() + currentRoom.getWidth() + roomDistance);
                            break;
                        }
                    }

                    ConnectionLine connectionLine = lineList.findByStartAndEndRoomIgnoreLineDirection(currentRoom,
                            newRoom);
                    if (connectionLine == null) {
                        // create a new line
                        connectionLine = new ConnectionLine(currentRoom, newRoom);
                        connectionLine.setInvalidationRunnable(lineInvalidationRunnable);
                        lineList.add(connectionLine);

                        final Line connectionLineCopy = connectionLine;
                        Platform.runLater(() -> drawing.getChildren().add(connectionLineCopy));
                    }

                    ConnectionLine finalConnectionLine = connectionLine;
                    Platform.runLater(finalConnectionLine::updateLocation);

                    if (!newRoom.isRendered()) {
                        // render the child
                        renderQueue.add(newRoom);
                    }
                }
            }

            // set the room count
            currentRoomCount = allRoomsAsList.size();
            allRoomsAsListCopy = null;
        });
    }

    @FXML
    // This method is called by the FXMLLoader when initialization is complete
    void initialize() {
        assert insertRoom != null : "fx:id=\"insertRoom\" was not injected: check your FXML file 'EditorMain.fxml'.";
        assert drawing != null : "fx:id=\"drawing\" was not injected: check your FXML file 'EditorMain.fxml'.";
        assert insertPath != null : "fx:id=\"insertPath\" was not injected: check your FXML file 'EditorMain.fxml'.";

        currentEditorInstance = this;

        // modify the default exception handler to show the ReportingDialog on every uncaught exception
        final Thread.UncaughtExceptionHandler currentUncaughtExceptionHandler = Thread
                .getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
            if (currentUncaughtExceptionHandler != null) {
                // execute current handler as we only want to append it
                currentUncaughtExceptionHandler.uncaughtException(thread, exception);
            }
            Platform.runLater(() -> {
                new ExceptionAlert(exception).showAndWait();
                new ReportingDialog(stage.getScene()).show(AppConfig.gitHubUserName, AppConfig.gitHubRepoName,
                        exception);
            });
        });

        currentGame.addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                newValue.modifiedProperty().addListener((observable1, oldValue1, newValue1) -> {
                    this.menuItemSave.setDisable(!newValue1);
                    setWindowTitle(newValue);
                });
            }
            setWindowTitle(newValue);
        });

        initGame();

        scrollPane.hvalueProperty().addListener((observable, oldValue, newValue) -> unselectingDisabled = true);
        scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> unselectingDisabled = true);

        // add button icons
        insertRoom.setGraphic(new ImageView(new Image(EditorView.class.getResourceAsStream("add-room.png"))));
        moveButton.setGraphic(new ImageView(new Image(EditorView.class.getResourceAsStream("move-arrows.png"))));
        insertPath.setGraphic(
                new ImageView(new Image(EditorView.class.getResourceAsStream("connecting-points.png"))));
        autoLayoutButton
                .setGraphic(new ImageView(new Image(EditorView.class.getResourceAsStream("autoLayout.png"))));
        refreshViewButton
                .setGraphic(new ImageView(new Image(EditorView.class.getResourceAsStream("refreshView.png"))));

        // add tooltips
        insertRoom.setTooltip(new Tooltip("Insert a new room"));
        moveButton.setTooltip(new Tooltip("Move rooms"));
        insertPath.setTooltip(new Tooltip("Connect rooms to create walk paths"));
        autoLayoutButton.setTooltip(new Tooltip("Automatically rearrange the rooms in the view below"));
        refreshViewButton.setTooltip(new Tooltip("Refresh the current view"));

        // forward events to all selected items
        // drawing.setOnMouseClicked(forwardEventsToSelectableNodesHandler);
        // drawing.setOnMousePressed(forwardEventsToSelectableNodesHandler);
        // drawing.setOnMouseReleased(forwardEventsToSelectableNodesHandler);
        // drawing.setOnDragDetected(forwardEventsToSelectableNodesHandler);
        // drawing.setOnMouseDragged(forwardEventsToSelectableNodesHandler);
        scrollPane.setOnKeyPressed(event -> {
            if (event.getCode().equals(KeyCode.DELETE)) {
                for (Node child : new ArrayList<>(drawing.getChildren())) {
                    if (child instanceof Disposable) {
                        if (((Disposable) child).isSelected() && event.getTarget() != child) {
                            FOKLogger.fine(EditorView.class.getName(),
                                    "Sending disposal command to child, Child is:  " + child.toString()
                                            + "\ntarget is: " + event.getTarget().toString());
                            try {
                                ((Disposable) child).dispose();
                            } catch (IllegalStateException e) {
                                FOKLogger.log(EditorView.class.getName(), Level.INFO,
                                        "User tried to remove the current room (not allowed)", e);
                                new Alert(Alert.AlertType.ERROR, "Could not perform delete operation: \n\n"
                                        + ExceptionUtils.getRootCauseMessage(e)).show();
                            }
                        }
                    }
                }
            } else if (event.getCode().equals(KeyCode.A) && event.isControlDown()) {
                // select everything
                for (Node child : new ArrayList<>(drawing.getChildren())) {
                    if (child instanceof Selectable) {
                        ((Selectable) child).setSelected(true);
                    }
                }
            }
        });
    }

    /**
     * Initializes a new game
     */
    public void initGame() {
        Game game = new Game();
        game.getCurrentRoom().setName("startRoom");
        loadGame(game);
    }

    /**
     * Loads the specified game file to this gui
     *
     * @param file The file to load
     */
    public void loadGame(File file) throws IOException, ClassNotFoundException {
        Game game = Game.load(file);
        loadGame(game);
    }

    /**
     * Loads the specified game to this gui
     *
     * @param game The game to load
     */
    public void loadGame(Game game) {
        currentGame.setValue(game);
        unconnectedRooms = new RoomRectangleList();
        allRoomsAsList = null;
        for (ConnectionLine line : new ConnectionLineList(lineList)) {
            line.invalidate();
        }
        renderView();
    }

    @SuppressWarnings("unused")
    public void setWindowTitle() {
        setWindowTitle(currentGame.getValue());
    }

    public void setWindowTitle(Game game) {
        String title = bundle.getString("windowTitle");

        if (game != null) {
            title = title + " - ";

            if (game.getFileSource() == null) {
                title = title + "unsaved game";
            } else {
                title = title + game.getFileSource().getName();
            }

            if (game.isModified()) {
                title = title + "*";
            }
        }

        stage.setTitle(title);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Platform.setImplicitExit(true);
        bundle = ResourceBundle.getBundle("view.strings");
        stage = primaryStage;

        try {
            Parent root = FXMLLoader.load(getClass().getResource("EditorMain.fxml"), bundle);

            Scene scene = new Scene(root);
            scene.getStylesheets().add(getClass().getResource("EditorMain.css").toExternalForm());

            primaryStage.setMinWidth(scene.getRoot().minWidth(0) + 70);
            primaryStage.setMinHeight(scene.getRoot().minHeight(0) + 70);

            primaryStage.setScene(scene);

            // Set Icon
            primaryStage.getIcons().add(new Image(MainWindow.class.getResourceAsStream("icon.png")));

            primaryStage.show();
        } catch (Exception e) {
            FOKLogger.log(MainWindow.class.getName(), Level.SEVERE, "An error occurred", e);
        }
    }

    @Override
    public void stop() {
        renderThreadPool.shutdownNow();

        // We need to call that explicitly because the ExecutorService makes the default exit bug around
        System.exit(0);
    }

    public EditMode getCurrentEditMode() {
        return currentEditMode;
    }

    public void setCurrentEditMode(EditMode currentEditMode) {
        FOKLogger.finer(MainWindow.class.getName(), "Setting currentEditMode to " + currentEditMode.toString());
        previousEditMode = this.currentEditMode;

        // Initialize or terminate the insert room mode
        if (isMouseOverDrawing) {
            if (currentEditMode == EditMode.INSERT_ROOM && previousEditMode != EditMode.INSERT_ROOM) {
                initInsertRoomEditMode();
            } else if (currentEditMode != EditMode.INSERT_ROOM && previousEditMode == EditMode.INSERT_ROOM) {
                terminateInsertRoomEditMode();
            }
        }

        this.currentEditMode = currentEditMode;
        this.insertPath.setSelected(currentEditMode == EditMode.INSERT_PATH);
        this.moveButton.setSelected(currentEditMode == EditMode.MOVE);
        this.insertRoom.setSelected(currentEditMode == EditMode.INSERT_ROOM);

    }

    public EditMode getPreviousEditMode() {
        return previousEditMode;
    }

    public RoomRectangleList getAllRoomsAsList() {
        if (allRoomsAsListCopy != null) {
            return allRoomsAsListCopy;
        } else {
            return allRoomsAsList;
        }
    }

    public RoomRectangleList getUnconnectedRooms() {
        return unconnectedRooms;
    }

    public Game getCurrentGame() {
        return currentGame.get();
    }

    @SuppressWarnings("unused")
    public ObjectProperty<Game> currentGameProperty() {
        return currentGame;
    }
}