<commit_before>package moonlightowl.openblocks;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import moonlightowl.openblocks.io.JSON;
import moonlightowl.openblocks.io.lua.Lua;
import moonlightowl.openblocks.structure.Block;
import moonlightowl.openblocks.structure.Joint;
import moonlightowl.openblocks.structure.Wire;
import moonlightowl.openblocks.ui.About;
import moonlightowl.openblocks.ui.ToolButton;
import moonlightowl.openblocks.ui.ToolPane;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;

/**
 * OpenBlocks.OpenBlocks
 * Created by MoonlightOwl on 10/25/15.
 * ===
 * Entry point and main window
 */

public class OpenBlocks extends Application {
    private Stage parentStage;
    // FXML links
    public AnchorPane rootPane;
    public MenuBar menuBar;
    public HBox toolBar;
    public ScrollPane scroller;
    // Custom elements
    private ToolPane[] tools;
    private About about;
    private Workspace workspace;

    private boolean selectedTrash = false;
    private Blocks.Id selected;
    private ImageView selectedIcon;
    private ArrayList<Wire> wires;
    private boolean addWires = false;

    private File projectFile;
    private boolean changed = false;

    @Override
    public void start(Stage primaryStage) throws Exception {
        parentStage = primaryStage;

        FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"));
        loader.setController(this);
        loader.load();

        // Resources
        Assets.load();
        Log.init();

        // Generate GUI
        initUI();

        // Create IDE window
        setTitle(Settings.UNTITLED);
        primaryStage.setScene(new Scene(rootPane, Settings.WIDTH, Settings.HEIGHT));
        primaryStage.setMinWidth(500);
        primaryStage.setMinHeight(400);
        primaryStage.show();

        // Handle exit
        primaryStage.setOnCloseRequest(event -> {
            if(changed) {
                // Ask a question
                Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
                alert.setTitle("Завершение работы");
                alert.setHeaderText("Сохранение изменений");
                alert.setContentText("В проект были внесены изменения. Сохранить?");

                ButtonType buttonSave = new ButtonType("Сохранить");
                ButtonType buttonDiscard = new ButtonType("Не сохранять");
                ButtonType buttonCancel = new ButtonType("Отмена", ButtonBar.ButtonData.CANCEL_CLOSE);

                alert.getButtonTypes().setAll(buttonSave, buttonDiscard, buttonCancel);

                // Get an answer
                Optional<ButtonType> result = alert.showAndWait();
                if (result.get() == buttonSave){
                    saveProject();
                } else if (result.get() == buttonCancel) {
                    event.consume();
                }
            }

            // Finalize resources
            if(!event.isConsumed()){
                Log.out("Shutdown.");
                Log.close();
            }
        });

        Log.out("Let's go!");
    }
    public static void main(String[] args) {
        launch(args);
    }


    public void setTitle(String title){
        if(title != null)
            parentStage.setTitle(title + " - " + Settings.TITLE + " " + Settings.VERSION);
        else
            parentStage.setTitle(Settings.TITLE + " " + Settings.VERSION);
        changed = false;
    }
    public void projectChanged() {
        if(!changed){
            parentStage.setTitle("*"+parentStage.getTitle());
            changed = true;
        }
    }


    public void initUI(){
        about = new About(parentStage);
        workspace = new Workspace(scroller);

        selectedIcon = new ImageView();
        selectedIcon.setScaleX(0.3);
        selectedIcon.setScaleY(0.3);
        rootPane.getChildren().add(selectedIcon);

        initToolsPanels();
        initToolBar();

        wires = new ArrayList<>();

        /** Event listenters */
        // Wire operations
        Joint.setOnClickListenter(event ->{
            Joint joint = (Joint)event.getSource();
            if(event.getButton() == MouseButton.PRIMARY) {
                // Replace existing one
                if(joint.isAttached()){
                    Wire[] old = new Wire[]{};
                    if(!joint.isMultiwired() || wires.isEmpty()){
                        old = joint.getWires();
                        joint.detachAllWires();
                    }
                    if(!addWires) {
                        wires.forEach(joint::attachWire);
                        wires.clear();
                    }
                    wires.addAll(Arrays.asList(old));
                }
                // Or create / attach new wire
                else {
                    if (wires.isEmpty()) {
                        Wire wire = new Wire();
                        joint.attachWire(wire);
                        wire.reposition(joint.getAbsX(), joint.getAbsY());
                        workspace.addWire(wire);
                        wires.add(wire);
                    } else {
                        wires.forEach(joint::attachWire);
                        wires.clear();
                    }
                }
                projectChanged();
            }
        });
        // Block removement
        Block.setOnClickListenter(event -> {
            if (selectedTrash) {
                Block block = (Block)event.getSource();
                workspace.removeBlock(block);
                projectChanged();
            }
        });
        // Wire removement
        Wire.setOnClickListenter(event -> {
            if (selectedTrash) {
                Wire wire = (Wire)event.getSource();
                workspace.removeWire(wire);
                projectChanged();
            }
        });

        // Add new objects to workspace
        rootPane.setOnMouseClicked(event -> {
            if(hasOpenedPanes())
                closeAllToolPanes();
            else
            if(event.getButton() == MouseButton.PRIMARY) {
                if(selected != null) {
                    Block block = selected.getInstance()
                            .setPosition(workspace.projectX(event.getX()),
                                    workspace.projectY(event.getY()));
                    workspace.addBlock(block);
                    projectChanged();
                }
            } else if(event.getButton() == MouseButton.SECONDARY) {
                // Deselect current block type
                deselect();
                // Remove current uncomplete wire
                if(!wires.isEmpty()){
                    wires.forEach(workspace::removeWire);
                    wires.clear();
                }
            }
        });

        // Move mouse tool icon & current wire loose end (if any)
        rootPane.setOnMouseMoved(event -> {
            selectedIcon.setTranslateX(event.getSceneX());
            selectedIcon.setTranslateY(event.getSceneY());
            if(!wires.isEmpty())
                for(Wire wire: wires)
                    wire.reposition(workspace.projectX(event.getX()),
                                    workspace.projectY(event.getY()));
        });
        rootPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, rootPane.getOnMouseMoved());

        // Keyboard tool selector
        rootPane.setOnKeyPressed(event -> {
            switch(event.getCode()){
                case DIGIT1: toggleToolPane(0); break;
                case DIGIT2: toggleToolPane(1); break;
                case DIGIT3: toggleToolPane(2); break;
                case DIGIT4: toggleToolPane(3); break;
                case S: workspace.drag(0, -Settings.DRAG_SPEED); break;
                case W: workspace.drag(0, Settings.DRAG_SPEED); break;
                case D: workspace.drag(-Settings.DRAG_SPEED, 0); break;
                case A: workspace.drag(Settings.DRAG_SPEED, 0); break;
                case SHIFT: addWires = true;
            }
        });
        rootPane.setOnKeyReleased(event -> {
            switch (event.getCode()) {
                case SHIFT: addWires = false;
            }
        });
    }

    /** UI generation */
    private void initToolsPanels(){
        tools = new ToolPane[] {
                new ToolPane("Точки входа"),
                new ToolPane("Действия"),
                new ToolPane("Циклы"),
                new ToolPane("Логика")
        };
        for(Blocks.Id id: Blocks.Id.values()){
            ToolButton tool = new ToolButton(id.name, Assets.toolIcons[id.id]);
            tool.setOnMouseClicked(event -> { select(id); closeAllToolPanes(); });
            tools[id.category.ordinal()].add(tool);
        }
        for(ToolPane pane: tools) rootPane.getChildren().add(pane);
    }
    private void initToolBar(){
        for(int c = 0; c < 4; c++) {
            Button button = newToolBarButton(Assets.toolBarIcon[c+1]);
            int id = c; button.setOnAction(event -> toggleToolPane(id));
            toolBar.getChildren().add(button);
        }
        Button trash = newToolBarButton(Assets.toolBarIcon[5]);
        trash.setOnAction(event -> selectTrashTool());
        toolBar.getChildren().add(trash);
    }
    private Button newToolBarButton(Image image){
        Button button = new Button();
        button.setId("tool");
        button.setGraphic(new ImageView(image));
        return button;
    }

    /** Tool actions */
    public void selectTrashTool(){
        deselect();
        selectedTrash = true;
        selectedIcon.setImage(Assets.toolBarIcon[5]);
    }
    public void select(Blocks.Id id){
        deselect();
        selected = id;
        selectedIcon.setImage(Assets.toolIcons[id.id]);
    }
    public void deselect(){
        selected = null;
        selectedTrash = false;
        selectedIcon.setImage(null);
    }

    /** Block panel actions */
    public void closeAllToolPanes(){ toggleToolPane(-1); }
    public void toggleToolPane(int id){
        for(int c = 0; c < tools.length; c++)
            if(c == id) tools[c].toggle();
            else tools[c].close();
    }
    public boolean hasOpenedPanes(){ for(ToolPane pane: tools) if(pane.isOpen()) return true; return false; }


    /** Project file management */
    private void save() {
        String data = JSON.generate(workspace).toJSONString();
        if(projectFile != null) {
            try (FileWriter writer = new FileWriter(projectFile)) {
                writer.write(data);
                Log.out("Successfully saved JSON Object to file...");
            } catch (IOException e) {
                Log.error("Project saving error", e);
                error("Ошибка записи проекта",
                        "В силу неведомых причин, сериализация проекта в JSON прошла неудачно.\n" +
                                "Проверьте, есть ли свободное место на диске, и имеет ли программа права на запись" +
                                "в выбранном каталоге.", e);
            }
        } else {
            error("Ошибка записи проекта", "Не удалось выбрать имя файла", null);
        }
    }
    private void load() {
        JSONParser parser = new JSONParser();
        try (FileReader reader = new FileReader(projectFile)) {
            JSONObject data = (JSONObject) parser.parse(reader);
            JSON.recreate(workspace, data);
        } catch (Exception e) {
            Log.error("Error loading project", e);
            error("Ошибка открытия проекта",
                    "Вероятнее всего, файл, который вы пытаетесь открыть - поврежден.", e);
        }
    }

    /** Menu actions */
    public void newProject() {
        workspace.clear();
        projectFile = null;
        setTitle(Settings.UNTITLED);
    }
    public void openProject(){
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Открыть...");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.setSelectedExtensionFilter(
                new FileChooser.ExtensionFilter("Проект "+Settings.TITLE, "*."+Settings.EXTENSION));
        projectFile = fileChooser.showOpenDialog(parentStage);
        if(projectFile != null) {
            workspace.clear();
            load();
            setTitle(projectFile.getName());
        }
    }
    public void saveProject() {
        if(projectFile == null) saveProjectAs();
        else{ save(); setTitle(projectFile.getName()); }
    }
    public void saveProjectAs() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Сохранить как...");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.setInitialFileName(projectFile == null ?
                Settings.UNTITLED + "." + Settings.EXTENSION : projectFile.getName());
        fileChooser.setSelectedExtensionFilter(
                new FileChooser.ExtensionFilter("Проект "+Settings.TITLE, "*."+Settings.EXTENSION));
        projectFile = fileChooser.showSaveDialog(parentStage);
        if(projectFile != null) {
            save();
            setTitle(projectFile.getName());
        }
    }
    public void exportProject() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Экспортировать в...");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.setInitialFileName((projectFile == null ?
                Settings.UNTITLED : projectFile.getName().split("\\.")[0]) + ".lua");
        fileChooser.setSelectedExtensionFilter(
                new FileChooser.ExtensionFilter("Программа OpenComputers", "*.lua"));
        File file = fileChooser.showSaveDialog(parentStage);
        try (FileOutputStream stream = new FileOutputStream(file)) {
            if(!Lua.export(workspace, stream))
                error("Ошибка экспорта",
                        "Внезапно, структура проекта не поддается экпорту! \nОтправьте проект автору IDE, пусть тоже удивится.", null);
            else
                Log.out("Successfully exported project to Lua");
        } catch (IOException e) {
            Log.error("Cannot export project properly! Some errors occured.", e);
            error("Ошибка записи проекта",
                    "В силу неизвестных причин, произошла ошибка записи экспортированного листинга в файл.", e);
        }
    }

    public void clearProject(){
        workspace.clear();
        projectChanged();
    }

    public void showAboutWindow() {<commit_msg>Tools management improvement<commit_after>package moonlightowl.openblocks;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import moonlightowl.openblocks.io.JSON;
import moonlightowl.openblocks.io.lua.Lua;
import moonlightowl.openblocks.structure.Block;
import moonlightowl.openblocks.structure.Joint;
import moonlightowl.openblocks.structure.Wire;
import moonlightowl.openblocks.ui.About;
import moonlightowl.openblocks.ui.ToolButton;
import moonlightowl.openblocks.ui.ToolPane;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;

/**
 * OpenBlocks.OpenBlocks
 * Created by MoonlightOwl on 10/25/15.
 * ===
 * Entry point and main window
 */

public class OpenBlocks extends Application {
    private Stage parentStage;
    // FXML links
    public AnchorPane rootPane;
    public MenuBar menuBar;
    public HBox toolBar;
    public ScrollPane scroller;
    // Custom elements
    private ToolPane[] tools;
    private About about;
    private Workspace workspace;

    private boolean selectedTrash = false;
    private Blocks.Id selected;
    private ImageView selectedIcon;
    private ArrayList<Wire> wires;
    private boolean addWires = false;

    private File projectFile;
    private boolean changed = false;

    @Override
    public void start(Stage primaryStage) throws Exception {
        parentStage = primaryStage;

        FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"));
        loader.setController(this);
        loader.load();

        // Resources
        Assets.load();
        Log.init();

        // Generate GUI
        initUI();

        // Create IDE window
        setTitle(Settings.UNTITLED);
        primaryStage.setScene(new Scene(rootPane, Settings.WIDTH, Settings.HEIGHT));
        primaryStage.setMinWidth(500);
        primaryStage.setMinHeight(400);
        primaryStage.show();

        // Handle exit
        primaryStage.setOnCloseRequest(event -> {
            if(changed) {
                // Ask a question
                Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
                alert.setTitle("Завершение работы");
                alert.setHeaderText("Сохранение изменений");
                alert.setContentText("В проект были внесены изменения. Сохранить?");

                ButtonType buttonSave = new ButtonType("Сохранить");
                ButtonType buttonDiscard = new ButtonType("Не сохранять");
                ButtonType buttonCancel = new ButtonType("Отмена", ButtonBar.ButtonData.CANCEL_CLOSE);

                alert.getButtonTypes().setAll(buttonSave, buttonDiscard, buttonCancel);

                // Get an answer
                Optional<ButtonType> result = alert.showAndWait();
                if (result.get() == buttonSave){
                    saveProject();
                } else if (result.get() == buttonCancel) {
                    event.consume();
                }
            }

            // Finalize resources
            if(!event.isConsumed()){
                Log.out("Shutdown.");
                Log.close();
            }
        });

        Log.out("Let's go!");
    }
    public static void main(String[] args) {
        launch(args);
    }


    public void setTitle(String title){
        if(title != null)
            parentStage.setTitle(title + " - " + Settings.TITLE + " " + Settings.VERSION);
        else
            parentStage.setTitle(Settings.TITLE + " " + Settings.VERSION);
        changed = false;
    }
    public void projectChanged() {
        if(!changed){
            parentStage.setTitle("*"+parentStage.getTitle());
            changed = true;
        }
    }


    public void initUI(){
        about = new About(parentStage);
        workspace = new Workspace(scroller);

        selectedIcon = new ImageView();
        selectedIcon.setScaleX(0.3);
        selectedIcon.setScaleY(0.3);
        rootPane.getChildren().add(selectedIcon);

        initToolsPanels();
        initToolBar();

        wires = new ArrayList<>();

        /** Event listenters */
        // Wire operations
        Joint.setOnClickListenter(event ->{
            Joint joint = (Joint)event.getSource();
            if(event.getButton() == MouseButton.PRIMARY) {
                // Replace existing one
                if(joint.isAttached()){
                    Wire[] old = new Wire[]{};
                    if(!joint.isMultiwired() || wires.isEmpty()){
                        old = joint.getWires();
                        joint.detachAllWires();
                    }
                    if(!addWires) {
                        wires.forEach(joint::attachWire);
                        wires.clear();
                    }
                    wires.addAll(Arrays.asList(old));
                }
                // Or create / attach new wire
                else {
                    if (wires.isEmpty()) {
                        Wire wire = new Wire();
                        joint.attachWire(wire);
                        wire.reposition(joint.getAbsX(), joint.getAbsY());
                        workspace.addWire(wire);
                        wires.add(wire);
                    } else {
                        wires.forEach(joint::attachWire);
                        wires.clear();
                    }
                }
                projectChanged();
            }
        });
        // Block removement
        Block.setOnClickListenter(event -> {
            if (selectedTrash && event.getButton() == MouseButton.PRIMARY) {
                Block block = (Block)event.getSource();
                workspace.removeBlock(block);
                projectChanged();
            }
        });
        // Wire removement
        Wire.setOnClickListenter(event -> {
            if (selectedTrash && event.getButton() == MouseButton.PRIMARY) {
                Wire wire = (Wire)event.getSource();
                workspace.removeWire(wire);
                projectChanged();
            }
        });

        // Add new objects to workspace
        rootPane.setOnMouseClicked(event -> {
            if(hasOpenedPanes())
                closeAllToolPanes();
            else
            if(event.getButton() == MouseButton.PRIMARY) {
                if(selected != null) {
                    Block block = selected.getInstance()
                            .setPosition(workspace.projectX(event.getX()),
                                    workspace.projectY(event.getY()));
                    workspace.addBlock(block);
                    projectChanged();
                }
            } else if(event.getButton() == MouseButton.SECONDARY) {
                // Deselect current block type
                deselect();
                // Remove current uncomplete wire
                if(!wires.isEmpty()){
                    wires.forEach(workspace::removeWire);
                    wires.clear();
                }
            }
        });

        // Move mouse tool icon & current wire loose end (if any)
        rootPane.setOnMouseMoved(event -> {
            selectedIcon.setTranslateX(event.getSceneX());
            selectedIcon.setTranslateY(event.getSceneY());
            if(!wires.isEmpty())
                for(Wire wire: wires)
                    wire.reposition(workspace.projectX(event.getX()),
                                    workspace.projectY(event.getY()));
        });
        rootPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, rootPane.getOnMouseMoved());

        // Keyboard tool selector
        rootPane.setOnKeyPressed(event -> {
            switch(event.getCode()){
                case DIGIT1: toggleToolPane(0); break;
                case DIGIT2: toggleToolPane(1); break;
                case DIGIT3: toggleToolPane(2); break;
                case DIGIT4: toggleToolPane(3); break;
                case S: workspace.drag(0, -Settings.DRAG_SPEED); break;
                case W: workspace.drag(0, Settings.DRAG_SPEED); break;
                case D: workspace.drag(-Settings.DRAG_SPEED, 0); break;
                case A: workspace.drag(Settings.DRAG_SPEED, 0); break;
                case SHIFT: addWires = true; break;
                case DELETE: selectTrashTool(); break;
                case ESCAPE: deselect(); break;
            }
        });
        rootPane.setOnKeyReleased(event -> {
            switch (event.getCode()) {
                case SHIFT: addWires = false;
            }
        });
    }

    /** UI generation */
    private void initToolsPanels(){
        tools = new ToolPane[] {
                new ToolPane("Точки входа"),
                new ToolPane("Действия"),
                new ToolPane("Циклы"),
                new ToolPane("Логика")
        };
        for(Blocks.Id id: Blocks.Id.values()){
            ToolButton tool = new ToolButton(id.name, Assets.toolIcons[id.id]);
            tool.setOnMouseClicked(event -> { select(id); closeAllToolPanes(); });
            tools[id.category.ordinal()].add(tool);
        }
        for(ToolPane pane: tools) rootPane.getChildren().add(pane);
    }
    private void initToolBar(){
        for(int c = 0; c < 4; c++) {
            Button button = newToolBarButton(Assets.toolBarIcon[c+1]);
            int id = c; button.setOnAction(event -> toggleToolPane(id));
            toolBar.getChildren().add(button);
        }
        Button trash = newToolBarButton(Assets.toolBarIcon[5]);
        trash.setOnAction(event -> selectTrashTool());
        toolBar.getChildren().add(trash);
    }
    private Button newToolBarButton(Image image){
        Button button = new Button();
        button.setId("tool");
        button.setGraphic(new ImageView(image));
        return button;
    }

    /** Tool actions */
    public void selectTrashTool(){
        deselect();
        selectedTrash = true;
        selectedIcon.setImage(Assets.toolBarIcon[5]);
    }
    public void select(Blocks.Id id){
        deselect();
        selected = id;
        selectedIcon.setImage(Assets.toolIcons[id.id]);
    }
    public void deselect(){
        selected = null;
        selectedTrash = false;
        selectedIcon.setImage(null);
    }

    /** Block panel actions */
    public void closeAllToolPanes(){ toggleToolPane(-1); }
    public void toggleToolPane(int id){
        for(int c = 0; c < tools.length; c++)
            if(c == id) tools[c].toggle();
            else tools[c].close();
    }
    public boolean hasOpenedPanes(){ for(ToolPane pane: tools) if(pane.isOpen()) return true; return false; }


    /** Project file management */
    private void save() {
        String data = JSON.generate(workspace).toJSONString();
        if(projectFile != null) {
            try (FileWriter writer = new FileWriter(projectFile)) {
                writer.write(data);
                Log.out("Successfully saved JSON Object to file...");
            } catch (IOException e) {
                Log.error("Project saving error", e);
                error("Ошибка записи проекта",
                        "В силу неведомых причин, сериализация проекта в JSON прошла неудачно.\n" +
                                "Проверьте, есть ли свободное место на диске, и имеет ли программа права на запись" +
                                "в выбранном каталоге.", e);
            }
        } else {
            error("Ошибка записи проекта", "Не удалось выбрать имя файла", null);
        }
    }
    private void load() {
        JSONParser parser = new JSONParser();
        try (FileReader reader = new FileReader(projectFile)) {
            JSONObject data = (JSONObject) parser.parse(reader);
            JSON.recreate(workspace, data);
        } catch (Exception e) {
            Log.error("Error loading project", e);
            error("Ошибка открытия проекта",
                    "Вероятнее всего, файл, который вы пытаетесь открыть - поврежден.", e);
        }
    }

    /** Menu actions */
    public void newProject() {
        workspace.clear();
        projectFile = null;
        setTitle(Settings.UNTITLED);
        deselect(); // Drop all previously selected tools
    }
    public void openProject(){
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Открыть...");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.setSelectedExtensionFilter(
                new FileChooser.ExtensionFilter("Проект "+Settings.TITLE, "*."+Settings.EXTENSION));
        projectFile = fileChooser.showOpenDialog(parentStage);
        if(projectFile != null) {
            workspace.clear();
            load();
            setTitle(projectFile.getName());
            deselect(); // Drop all previously selected tools
        }
    }
    public void saveProject() {
        if(projectFile == null) saveProjectAs();
        else{ save(); setTitle(projectFile.getName()); }
    }
    public void saveProjectAs() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Сохранить как...");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.setInitialFileName(projectFile == null ?
                Settings.UNTITLED + "." + Settings.EXTENSION : projectFile.getName());
        fileChooser.setSelectedExtensionFilter(
                new FileChooser.ExtensionFilter("Проект "+Settings.TITLE, "*."+Settings.EXTENSION));
        projectFile = fileChooser.showSaveDialog(parentStage);
        if(projectFile != null) {
            save();
            setTitle(projectFile.getName());
        }
    }
    public void exportProject() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Экспортировать в...");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.setInitialFileName((projectFile == null ?
                Settings.UNTITLED : projectFile.getName().split("\\.")[0]) + ".lua");
        fileChooser.setSelectedExtensionFilter(
                new FileChooser.ExtensionFilter("Программа OpenComputers", "*.lua"));
        File file = fileChooser.showSaveDialog(parentStage);
        try (FileOutputStream stream = new FileOutputStream(file)) {
            if(!Lua.export(workspace, stream))
                error("Ошибка экспорта",
                        "Внезапно, структура проекта не поддается экпорту! \nОтправьте проект автору IDE, пусть тоже удивится.", null);
            else
                Log.out("Successfully exported project to Lua");
        } catch (IOException e) {
            Log.error("Cannot export project properly! Some errors occured.", e);
            error("Ошибка записи проекта",
                    "В силу неизвестных причин, произошла ошибка записи экспортированного листинга в файл.", e);
        }
    }

    public void clearProject(){
        workspace.clear();
        projectChanged();
        deselect(); // Drop all previously selected tools
    }

    public void showAboutWindow() {
{'fraction_non_alphanumeric': 0.11152731181394743, 'fraction_numerical': 0.002345200482068988, 'mean_word_length': 1.906010411736867, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1680215', 'n_tokens_mistral': 8524, 'n_tokens_neox': 8277, 'n_words': 1703}