org.sandsoft.acefx.AceEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.sandsoft.acefx.AceEditor.java

Source

/*
 * Copyright 2015 Sudipto Chandra.
 *
 * 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 org.sandsoft.acefx;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.concurrent.Worker;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.FileChooser;
import netscape.javascript.JSException;
import netscape.javascript.JSObject;
import org.sandsoft.acefx.model.Command;
import org.sandsoft.acefx.model.Editor;
import org.sandsoft.acefx.model.UndoManager;
import org.sandsoft.acefx.model.EditSession;
import org.sandsoft.acefx.model.ThemeData;
import org.sandsoft.acefx.util.Commons;
import org.apache.commons.io.FileUtils;
import org.sandsoft.acefx.model.ModeData;

/**
 * A fully functional self-sufficient code editor based on ACE. <br/><br/>
 * <b>Hints</b>:
 * <ul>
 * <li>To create a new editor: <code>AceEditor.createNew()</code></li>
 * <li>To listen to events:
 * <code>addEventHandler(AceEvents.YOUR_EVENT, YOUR_EVENT_HANDLER)</code></li>
 * </ul>
 *
 * @author Sudipto Chandra.
 */
public final class AceEditor extends BorderPane {

    //where ace.js file is saved
    private static final String ACE_PATH = "ace/ace.js";

    //ace controller
    private JSObject mAce;
    //current editor
    private Editor mEditor;
    //file path to save code
    private File mFilePath;

    //web view where editor is loaded
    @FXML
    private WebView webView;
    //web engine to process java script
    private WebEngine mWebEngine;

    @FXML
    private ComboBox themeListBox;
    @FXML
    private ComboBox modeListBox;
    @FXML
    private Button undoButton;
    @FXML
    private Button redoButton;

    /**
     * Constructor
     */
    public AceEditor() {
    }

    /**
     * Creates a new instance of the ace editor.
     *
     * @return
     * @throws java.io.IOException
     */
    public static AceEditor createNew() throws IOException {
        //init loader           
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(AceEditor.class.getResource(AceEditor.class.getSimpleName() + ".fxml"));

        //attach node
        Node node = (Node) loader.load();
        BorderPane.setAlignment(node, Pos.CENTER);
        AceEditor ace = (AceEditor) loader.getController();
        ace.setCenter(node);
        ace.setMinSize(0, 0);
        ace.setPrefSize(600, 400);
        ace.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);

        //post load work  
        ace.initialize();

        return ace;
    }

    /**
     * Initializes view and controls after FXML is loaded.
     */
    public void initialize() {
        //setup view   
        webView.setContextMenuEnabled(false);
        mWebEngine = webView.getEngine();
        loadAceEditor();
        loadModeList();
        loadThemeList();

        // process page loading
        mWebEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> ov, Worker.State t, Worker.State t1) {
                if (mWebEngine.getLoadWorker().getState() == Worker.State.SUCCEEDED) {
                    //extract javascript objects
                    mAce = (JSObject) mWebEngine.executeScript("ace");
                    JSObject editor = (JSObject) mAce.call("edit", "editor");
                    mEditor = new Editor(editor);

                    setEventCatchers(editor);
                    setTheme(Themes.Chrome);
                    setMode(Modes.Text);

                    fireEvent(new Event(AceEvents.onLoadEvent));
                }
            }
        });
    }

    /**
     * Loads the ACE editor in the web engine.
     */
    private void loadAceEditor() {
        //get ace.js path
        String acepath = getClass().getResource(ACE_PATH).toExternalForm();
        //html wrapper
        String html = "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + "<head>\n" + "  <meta charset=\"UTF-8\">\n"
                + "  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n"
                + "  <title>Ace Editor In JavaFX</title>\n" + "  <style type=\"text/css\" media=\"screen\">\n"
                + "    body {\n" + "        overflow: hidden;\n" + "    }\n" + "    #editor {\n"
                + "        margin: 0;\n" + "        position: absolute;\n" + "        top: 0;\n"
                + "        bottom: 0;\n" + "        left: 0;\n" + "        right: 0;\n" + "    }\n" + "  </style>\n"
                + "</head>\n" + "<body>\n" + "<pre id=\"editor\"></pre>\n" //text to diplay in the editor
                + "<script src=\"%s\" " // %s should be replaced by the path where the ace.js is located.
                + "type=\"text/javascript\" charset=\"utf-8\"></script>\n" + "<script>\n"
                + "    var editor = ace.edit(\"editor\"); \n" + "    editor.setTheme(\"ace/theme/twilight\");\n"
                + "    editor.getSession().setMode(\"ace/mode/java\");\n" + "</script>\n" + "</body>\n"
                + "</html>\n";
        //load html
        mWebEngine.loadContent(String.format(html, acepath));
    }

    /**
     * Creates event listener. <br/>
     * This uses the 'upcall' feature from java-script to java.
     *
     * @param editor
     */
    private void setEventCatchers(JSObject editor) {
        //set interface object
        editor.setMember("mAceEvent", new AceEvents(this));

        //on editor events
        editor.eval("this.on('blur', function() { editor.mAceEvent.onBlur(); });");
        editor.eval("this.on('change', function(e) { editor.mAceEvent.onChange(e); });");
        editor.eval(
                "this.on('changeSelectionStyle', function(e) { editor.mAceEvent.onChangeSelectionStyle(e); });");
        editor.eval("this.on('changeSession', function(e) { editor.mAceEvent.onChangeSession(e); });");
        editor.eval("this.on('copy', function(e) { editor.mAceEvent.onCopy(e); });");
        editor.eval("this.on('focus', function() { editor.mAceEvent.onFocus(); });");
        editor.eval("this.on('paste', function(e) { editor.mAceEvent.onPaste(e); });");

        //on edit session events
        editor.eval(
                "this.getSession().on('changeAnnotation', function() { editor.mAceEvent.onChangAnnotation(); });");
        editor.eval(
                "this.getSession().on('changeBackMarker', function() { editor.mAceEvent.onChangeBackMarker(); });");
        editor.eval(
                "this.getSession().on('changeBreakpoint', function() { editor.mAceEvent.onChangeBreakpoint(); });");
        editor.eval("this.getSession().on('changeFold', function() { editor.mAceEvent.onChangeFold(); });");
        editor.eval(
                "this.getSession().on('changeFrontMarker', function() { editor.mAceEvent.onChangeFrontMarker(); });");
        editor.eval("this.getSession().on('changeMode', function() { editor.mAceEvent.onChangeMode(); });");
        editor.eval(
                "this.getSession().on('changeOverwrite', function() { editor.mAceEvent.onChangeOverwrite(); });");
        editor.eval(
                "this.getSession().on('changeScrollLeft', function(e) { editor.mAceEvent.onChangeScrollLeft(e); });");
        editor.eval(
                "this.getSession().on('changeScrollTop', function(e) { editor.mAceEvent.onChangeScrollTop(e); });");
        editor.eval("this.getSession().on('changeTabSize', function() { editor.mAceEvent.onChangeTabSize(); });");
        editor.eval(
                "this.getSession().on('changeWrapLimit', function() { editor.mAceEvent.onChangeWrapLimit(); });");
        editor.eval("this.getSession().on('changeWrapMode', function() { editor.mAceEvent.onChangeWrapMode(); });");
        editor.eval(
                "this.getSession().on('tokenizerUpdate', function(e) { editor.mAceEvent.onTokenizerUpadate(e); });");

        addEventHandler(AceEvents.onChangeEvent, new EventHandler<Event>() {
            @Override
            public void handle(Event event) {
                undoButton.setDisable(!getUndoManager().hasUndo());
                redoButton.setDisable(!getUndoManager().hasRedo());
            }
        });
    }

    /**
     * Executes a script on the current web engine.
     *
     * @param script Script to execute.
     * @return
     */
    public Object executeScript(String script) throws JSException {
        return mWebEngine.executeScript(script);
    }

    /**
     * Gets the wrapper class for editor that is associated with this control.
     * It contains various methods to interact with the editor.
     *
     * @return the editor attached to this control.
     */
    public Editor getEditor() {
        return mEditor;
    }

    /**
     * Gets the wrapper class for edit session that is associated with the
     * editor. It contains various methods to interact with the document under
     * edit.
     *
     * @return the edit session for the editor.
     */
    public EditSession getSession() {
        return mEditor.getSession();
    }

    /**
     * Gets the wrapper class for undo manger that is associated with the
     * editor. It contains methods for undo or redo operations.
     *
     * @return the undo manager for the edit session.
     */
    public UndoManager getUndoManager() {
        return getSession().getUndoManager();
    }

    /**
     * Gets the current content from the editor. If the editor is not ready an
     * empty text is returned.
     *
     * @return Current content in the editor.
     */
    public String getText() {
        return mEditor.getValue();

    }

    /**
     * Sets the given content to the editor.
     *
     * @param text the content to display.
     */
    public void setText(String text) {
        getEditor().setValue(text, 1);
    }

    /**
     * Reloads the whole editor in WebView.
     */
    public void doReload() {
        loadAceEditor();
    }

    /**
     * Performs an undo operation. Reverts the changes.
     */
    public void doUndo() {
        getEditor().undo();
        undoButton.setDisable(!getUndoManager().hasUndo());
        redoButton.setDisable(!getUndoManager().hasRedo());
    }

    /**
     * Performs an redo operation. Re-implements the changes.
     */
    public void doRedo() {
        getEditor().redo();
        undoButton.setDisable(!getUndoManager().hasUndo());
        redoButton.setDisable(!getUndoManager().hasRedo());
    }

    /**
     * Paste text from clipboard after the cursor.
     */
    public void doPaste() {
        getEditor().insert(Clipboard.getSystemClipboard().getString());
    }

    /**
     * Copies the selected text to clipboard.
     *
     * @return True if performed successfully.
     */
    public boolean doCopy() {
        String copy = mEditor.getCopyText();
        if (copy != null && !copy.isEmpty()) {
            ClipboardContent content = new ClipboardContent();
            content.putString(copy);
            Clipboard.getSystemClipboard().setContent(content);
            return true;
        }
        return false;
    }

    /**
     * Removes the selected text and copy it to clipboard.
     */
    public void doCut() {
        if (doCopy()) {
            getEditor().insert("");
        }
    }

    /**
     * Shows the find dialog.
     */
    public void showFind() {
        getEditor().execCommand("find");
    }

    /**
     * Shows the replace dialog.
     */
    public void showReplace() {
        getEditor().execCommand("replace");
    }

    /**
     * Shows the options pane.
     */
    public void showOptions() {
        getEditor().execCommand("showSettingsMenu");
    }

    /**
     * Loads a content from a file.
     *
     * @param file File path to load.
     * @throws java.io.FileNotFoundException thrown if file could not be found.
     * @throws java.io.IOException thrown if file could no be read.
     */
    public void openFile(File file) throws FileNotFoundException, IOException {
        mFilePath = file;
        setText(FileUtils.readFileToString(file));
        setMode(Modes.getModeFromFile(file.getName()));
        //getUndoManager().reset(); 
        undoButton.setDisable(true);
        redoButton.setDisable(true);
    }

    /**
     * Saves the previously opened file.
     *
     * @throws java.io.IOException thrown if file could no be read.
     */
    public void saveFile() throws IOException, NullPointerException {
        FileUtils.writeStringToFile(mFilePath, getText());
    }

    /**
     * Change or set new save location and saves the file.
     *
     * @param file new location to save.
     * @throws java.io.IOException thrown if file could no be read.
     */
    public void saveAs(File file) throws IOException, NullPointerException {
        mFilePath = file;
        saveFile();
        setMode(Modes.getModeFromFile(file.getName()));
    }

    /**
     * Select the syntax highlighting mode for ace-editor. Some pre-defined
     * supported mode can be found in <code>Modes</code> class.
     *
     * @see Modes
     * @param mode Mode like "ace/mode/java".
     */
    public void setMode(ModeData mode) {
        modeListBox.getSelectionModel().select(mode);
    }

    /**
     * Currently enabled language mode.
     *
     * @see EditSession.getMode()
     * @return the current mode.
     */
    public ModeData getMode() {
        return Modes.getModeByAlias(getSession().getMode());
    }

    /**
     * Sets a theme to the editor. Some pre-defined can be found in
     * <code>Themes</code> class.
     *
     * @see Themes
     * @param theme Theme to set (must contain valid alias).
     */
    public void setTheme(ThemeData theme) {
        themeListBox.getSelectionModel().select(theme);
    }

    /**
     * Gets the current theme.
     *
     * @return
     */
    public ThemeData getTheme() {
        return Themes.getThemeByAlias(getEditor().getTheme());
    }

    /**
     * Gets a list of all available command and keyboard shortcuts
     *
     * @deprecated for internal usage only.
     * @return list of available commands
     */
    @Deprecated
    public ArrayList<Command> getCommandList() {
        JSObject names = (JSObject) mEditor.getModel().eval("this.commands.byName");
        ArrayList<Command> arr = new ArrayList<>();
        for (String str : Commons.getAllProperties(names)) {
            arr.add(new Command((JSObject) names.getMember(str)));
        }
        return arr;
    }

    //loads the list of themes in the themeListBox combobox in toolbar
    private void loadThemeList() {
        themeListBox.setItems(FXCollections.observableArrayList(Themes.SUPPORTED_THEMES));
        themeListBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
            @Override
            public void changed(ObservableValue observable, Object oldValue, Object newValue) {
                if (newValue != null && oldValue != newValue) {
                    Object data = themeListBox.getSelectionModel().getSelectedItem();
                    getEditor().setTheme(((ThemeData) data).getAlias());
                }
            }
        });
    }

    //loads the list of modes in the modeListBox combobox in toolbar
    private void loadModeList() {
        modeListBox.setItems(FXCollections.observableArrayList(Modes.SUPPORTED_MODES));
        modeListBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
            @Override
            public void changed(ObservableValue observable, Object oldValue, Object newValue) {
                if (newValue != null && oldValue != newValue) {
                    Object data = modeListBox.getSelectionModel().getSelectedItem();
                    getEditor().getSession().setMode(((ModeData) data).getAlias());
                }
            }
        });
    }

    //load file extension filters
    private void attachFilters(FileChooser fileChooser) {
        FileChooser.ExtensionFilter def = new FileChooser.ExtensionFilter("All Files", "*.*");
        fileChooser.getExtensionFilters().add(def);
        fileChooser.setSelectedExtensionFilter(def);
        for (ModeData md : Modes.SUPPORTED_MODES) {
            fileChooser.getExtensionFilters().add(md.getExtensionFilter());
        }
    }

    //toolbar buttons on action
    @FXML
    private void openButtonOnAction() {
        try {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("Open file");
            attachFilters(fileChooser);
            if (mFilePath != null) {
                fileChooser.setInitialFileName(mFilePath.getName());
                fileChooser.setInitialDirectory(mFilePath.getParentFile());
            }
            File file = fileChooser.showOpenDialog(this.getScene().getWindow());
            if (file != null) {
                openFile(file);
            }
        } catch (Exception ex) {
            Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
        }
    }

    @FXML
    private void saveButtonOnAction() {
        try {
            if (mFilePath != null) {
                saveFile();
                return;
            }
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("Save file");
            attachFilters(fileChooser);
            File file = fileChooser.showSaveDialog(this.getScene().getWindow());
            if (file != null) {
                saveAs(file);
            }
        } catch (IOException | NullPointerException ex) {
            Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
        }
    }

    @FXML
    private void reloadButtonOnAction() {
        doReload();
    }

    @FXML
    private void cutButtonOnAction() {
        doCut();
    }

    @FXML
    private void copyButtonOnAction() {
        doCopy();
    }

    @FXML
    private void pasteButtonOnAction() {
        doPaste();
    }

    @FXML
    private void undoButtonOnAction() {
        doUndo();
    }

    @FXML
    private void redoButtonOnAction() {
        doRedo();
    }

    @FXML
    private void findButtonOnAction() {
        showFind();
    }

    @FXML
    private void replaceButtonOnAction() {
        showReplace();
    }

    @FXML
    private void optionsButtonOnAction() {
        showOptions();
    }

}