be.makercafe.apps.makerbench.editors.JFXMillEditor.java Source code

Java tutorial

Introduction

Here is the source code for be.makercafe.apps.makerbench.editors.JFXMillEditor.java

Source

/*
Copyright 2015 - 2016, Luc De pauw - Makercafe.be
This file is part of Makerbench.
    
Makerbench 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.
    
Makerbench 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.
    
You should have received a copy of the GNU General Public License
along with Makerbench.  If not, see <http://www.gnu.org/licenses/>.
*/
package be.makercafe.apps.makerbench.editors;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.geometry.Orientation;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SnapshotParameters;
import javafx.scene.SubScene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Separator;
import javafx.scene.control.SplitPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToolBar;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.transform.Scale;
import javafx.stage.FileChooser;

import javax.imageio.ImageIO;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.StyleSpans;
import org.fxmisc.richtext.StyleSpansBuilder;
import org.reactfx.Change;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;

import be.makercafe.apps.makerbench.millcrum.Millcrum;
import de.jensd.fx.glyphs.GlyphsDude;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;

public class JFXMillEditor extends Editor {

    private static final String[] KEYWORDS = new String[] { "def", "in", "as", "abstract", "assert", "boolean",
            "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double",
            "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import",
            "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public",
            "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws",
            "transient", "try", "void", "volatile", "while" };

    private static final String KEYWORD_PATTERN = "\\b(" + String.join("|", KEYWORDS) + ")\\b";
    private static final String PAREN_PATTERN = "\\(|\\)";
    private static final String BRACE_PATTERN = "\\{|\\}";
    private static final String BRACKET_PATTERN = "\\[|\\]";
    private static final String SEMICOLON_PATTERN = "\\;";
    private static final String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";
    private static final String COMMENT_PATTERN = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/";

    private static final Pattern PATTERN = Pattern.compile("(?<KEYWORD>" + KEYWORD_PATTERN + ")" + "|(?<PAREN>"
            + PAREN_PATTERN + ")" + "|(?<BRACE>" + BRACE_PATTERN + ")" + "|(?<BRACKET>" + BRACKET_PATTERN + ")"
            + "|(?<SEMICOLON>" + SEMICOLON_PATTERN + ")" + "|(?<STRING>" + STRING_PATTERN + ")" + "|(?<COMMENT>"
            + COMMENT_PATTERN + ")");

    private final Group viewGroup;

    private CodeArea caCodeArea;

    private boolean autoCompile = false;

    private Millcrum millObject;

    private BorderPane editorContainer;

    private Pane viewContainer;

    private SubScene subScene;

    private ToolBar toolBar = null;

    private ComboBox cbxSourceExamples = null;

    public JFXMillEditor(String tabText, Path path) {
        super(tabText);

        this.viewGroup = new Group();
        this.editorContainer = new BorderPane();
        this.viewContainer = new Pane();

        this.caCodeArea = new CodeArea("");
        this.caCodeArea.setEditable(true);
        this.caCodeArea.setParagraphGraphicFactory(LineNumberFactory.get(caCodeArea));
        this.caCodeArea.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
        this.caCodeArea.getStylesheets().add(this.getClass().getResource("java-keywords.css").toExternalForm());
        this.caCodeArea.richChanges().subscribe(change -> {
            caCodeArea.setStyleSpans(0, computeHighlighting(caCodeArea.getText()));
        });
        addContextMenu(this.caCodeArea);

        EventStream<Change<String>> textEvents = EventStreams.changesOf(caCodeArea.textProperty());

        textEvents.reduceSuccessions((a, b) -> b, Duration.ofMillis(3000)).subscribe(code -> {
            if (autoCompile) {
                compile(code.getNewValue());
            }
        });

        try {
            this.caCodeArea.replaceText(FileUtils.readFileToString(path.toFile()));
        } catch (IOException ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Error reading file.", ex);
        }

        SplitPane editorPane = new SplitPane(caCodeArea, viewContainer);
        editorPane.setOrientation(Orientation.HORIZONTAL);
        BorderPane rootPane = new BorderPane();

        toolBar = createToolBar();
        rootPane.setTop(toolBar);
        rootPane.setCenter(editorPane);
        this.getTab().setContent(rootPane);

    }

    private static StyleSpans<Collection<String>> computeHighlighting(String text) {
        Matcher matcher = PATTERN.matcher(text);
        int lastKwEnd = 0;
        StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
        while (matcher.find()) {
            String styleClass = matcher.group("KEYWORD") != null ? "keyword"
                    : matcher.group("PAREN") != null ? "paren"
                            : matcher.group("BRACE") != null ? "brace"
                                    : matcher.group("BRACKET") != null ? "bracket"
                                            : matcher.group("SEMICOLON") != null ? "semicolon"
                                                    : matcher.group("STRING") != null ? "string"
                                                            : matcher.group("COMMENT") != null ? "comment"
                                                                    : null; /*
                                                                            * never
                                                                            * happens
                                                                            */
            assert styleClass != null;
            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();
        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }

    private void setCode(String code) {
        // this.codeArea.clear();
        this.caCodeArea.replaceText(code);

    }

    private String getCode() {
        return this.caCodeArea.getText();
    }

    private void compile(String code) {

        millObject = null;

        viewGroup.getChildren().clear();

        try {

            CompilerConfiguration cc = new CompilerConfiguration();

            cc.addCompilationCustomizers(
                    new ImportCustomizer().addStarImports("be.makercafe.apps.makerbench.millcrum", "javafx"));

            GroovyShell shell = new GroovyShell(getClass().getClassLoader(), new Binding(), cc);

            Script script = shell.parse(code);

            Object obj = script.run();

            if (obj instanceof Millcrum) {

                millObject = (Millcrum) obj;

                viewContainer.getChildren().clear();
                viewContainer.getChildren().add(millObject.getMillCanvas().getCanv());

            } else {
                Logger.getLogger(this.getClass().getName()).log(Level.INFO, "No Millcrum object returned");
            }

        } catch (Throwable ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.INFO, "Unsuspected exception", ex);
        }
    }

    // private void setMeshScale(MeshContainer meshContainer, Bounds t1, final
    // MeshView meshView) {
    // if (meshContainer != null) {
    // double maxDim = Math.max(meshContainer.getWidth(),
    // Math.max(meshContainer.getHeight(), meshContainer.getDepth()));
    //
    // double minContDim = Math.min(t1.getWidth(), t1.getHeight());
    //
    // double scale = minContDim / (maxDim * 2);
    //
    // meshView.setScaleX(scale);
    // meshView.setScaleY(scale);
    // meshView.setScaleZ(scale);
    // }
    // }

    /**
     * Creates the toolBar for the editor.
     *
     * @return
     */

    private ToolBar createToolBar() {

        ToolBar toolBar = new ToolBar();
        toolBar.setOrientation(Orientation.HORIZONTAL);

        Button btnSave = GlyphsDude.createIconButton(MaterialDesignIcon.FLOPPY, "Save");
        btnSave.setOnAction(this::handleSaveButton);

        Button btnExportSTL = GlyphsDude.createIconButton(MaterialDesignIcon.EXPORT, "Export GCODE");

        btnExportSTL.setOnAction(this::handleExportAsGCodeFile);

        Button btnExportPNG = GlyphsDude.createIconButton(MaterialDesignIcon.CAMERA, "Export PNG");
        btnExportPNG.setOnAction(this::handleExportAsPngFile);

        Button btnRun = GlyphsDude.createIconButton(MaterialDesignIcon.RUN, "Run");
        btnRun.setOnAction(this::handleCompileAndRun);

        ToggleButton btnAutoCompile = GlyphsDude.createIconToggleButton(MaterialDesignIcon.AUTO_FIX,
                "Automatic run", null, ContentDisplay.LEFT);
        btnAutoCompile.setOnAction(this::handleAutoCompile);
        btnAutoCompile.setSelected(false);

        ToggleButton btn3DNav = GlyphsDude.createIconToggleButton(MaterialDesignIcon.ROTATE_3D, "3D Navigation ",
                null, ContentDisplay.LEFT);
        btn3DNav.setSelected(false);

        ComboBox cbxSourceExamples = new ComboBox();
        cbxSourceExamples.getItems().addAll("TestCut");
        this.cbxSourceExamples = cbxSourceExamples; // TODO: maybe cleaner way
        // to do this ?

        Button btnPasteSource = GlyphsDude.createIconButton(MaterialDesignIcon.CONTENT_PASTE, "Paste source");
        btnPasteSource.setOnAction(this::handlePasteSource);

        toolBar.getItems().addAll(btnSave, btnExportSTL, btnExportPNG, new Separator(), btnRun, new Separator(),
                btnAutoCompile, new Separator(), cbxSourceExamples, btnPasteSource);

        return toolBar;

    }

    private void handleSaveButton(ActionEvent event) {
        System.out.println("Event: " + event.getSource());
        @SuppressWarnings("unchecked")
        Map<String, String> map = (Map<String, String>) this.getTab().getUserData();
        String path = map.get("path");
        try {
            FileUtils.writeStringToFile(new File(path), caCodeArea.getText());
        } catch (IOException e) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Unable to save file.");
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Oeps an error occured");
            alert.setHeaderText("Cannot save file. There went something wrong writing the file.");
            alert.setContentText(
                    "Please verify that your file is not read only, is not locked by other user or program, you have enough diskspace.");
            alert.showAndWait();
        }
    }

    private void handleExportAsGCodeFile(ActionEvent e) {

        if (millObject == null) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Oeps an error occured");
            alert.setHeaderText("Cannot export gcode. There is no geometry !");
            alert.setContentText("Please verify that your code generates a valid millcrum object.");
            alert.showAndWait();
            return;
        }

        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Export GCODE File");
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("GCODE files (*.gcode)", "*.tap"));

        File f = fileChooser.showSaveDialog(null);

        if (f == null) {
            return;
        }

        String fName = f.getAbsolutePath();

        if (!fName.toLowerCase().endsWith(".gcode")) {
            fName += ".gcode";
        }

        try {
            millObject.get();
            FileUtils.write(new File(fName), millObject.getToSaveGcode());
        } catch (IOException ex) {

            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Oeps an error occured");
            alert.setHeaderText("Cannot export gcode. There went something wrong writing the file.");
            alert.setContentText(
                    "Please verify that your file is not read only, is not locked by other user or program, you have enough diskspace.");
            alert.showAndWait();
        }
    }

    private void handleExportAsPngFile(ActionEvent e) {

        if (millObject == null) {
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Oeps an error occured");
            alert.setHeaderText("Cannot export PNG. There is no geometry !");
            alert.setContentText("Please verify that your code generates a valid CSG object.");
            alert.showAndWait();
            return;
        }

        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Export PNG File");
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image files (*.png)", "*.png"));

        File f = fileChooser.showSaveDialog(null);

        if (f == null) {
            return;
        }

        String fName = f.getAbsolutePath();

        if (!fName.toLowerCase().endsWith(".png")) {
            fName += ".png";
        }

        int snWidth = 1024;
        int snHeight = 1024;

        double realWidth = viewGroup.getBoundsInLocal().getWidth();
        double realHeight = viewGroup.getBoundsInLocal().getHeight();

        double scaleX = snWidth / realWidth;
        double scaleY = snHeight / realHeight;

        double scale = Math.min(scaleX, scaleY);

        PerspectiveCamera snCam = new PerspectiveCamera(false);
        snCam.setTranslateZ(-200);

        SnapshotParameters snapshotParameters = new SnapshotParameters();
        snapshotParameters.setTransform(new Scale(scale, scale));
        snapshotParameters.setCamera(snCam);
        snapshotParameters.setDepthBuffer(true);
        snapshotParameters.setFill(Color.TRANSPARENT);

        WritableImage snapshot = new WritableImage(snWidth, (int) (realHeight * scale));

        viewGroup.snapshot(snapshotParameters, snapshot);

        try {
            ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", new File(fName));
        } catch (IOException ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);
            Alert alert = new Alert(AlertType.ERROR);
            alert.setTitle("Oeps an error occured");
            alert.setHeaderText("Cannot export PNG. There went something wrong writing the file.");
            alert.setContentText(
                    "Please verify that your file is not read only, is not locked by other user or program, you have enough diskspace.");
            alert.showAndWait();
        }
    }

    private void handleCompileAndRun(ActionEvent e) {
        compile(getCode());
    }

    private void handleAutoCompile(ActionEvent e) {
        this.autoCompile = !this.autoCompile;
    }

    private void handlePasteSource(ActionEvent e) {
        String exampleSourceCode = null;
        try {
            exampleSourceCode = (String) this.cbxSourceExamples.getSelectionModel().getSelectedItem();
            String code = IOUtils.toString(this.getClass().getResourceAsStream(exampleSourceCode + ".jfxmill"),
                    "UTF-8");
            this.caCodeArea.replaceText(code);
        } catch (IOException ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,
                    "Unable to load example source code: " + exampleSourceCode, ex);
        }
    }

    public ToolBar getToolBar() {
        return toolBar;
    }

    public void setToolbar(ToolBar toolBar) {
        this.toolBar = toolBar;
    }

}