processing.app.EditorTab.java Source code

Java tutorial

Introduction

Here is the source code for processing.app.EditorTab.java

Source

/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Arduino project - http://www.arduino.cc
    
  Copyright (c) 2015 Matthijs Kooijman
  Copyright (c) 2004-09 Ben Fry and Casey Reas
  Copyright (c) 2001-04 Massachusetts Institute of Technology
    
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2
  as published by the Free Software Foundation.
    
  This program 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 this program; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package processing.app;

import static processing.app.I18n.tr;
import static processing.app.Theme.scale;

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseWheelListener;
import java.awt.event.MouseWheelEvent;

import java.io.IOException;

import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.ToolTipManager;
import javax.swing.border.MatteBorder;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.PlainDocument;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;

import org.apache.commons.lang3.StringUtils;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit;
import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
import org.fife.ui.rtextarea.Gutter;
import org.fife.ui.rtextarea.RTextScrollPane;

import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
import processing.app.helpers.DocumentTextChangeListener;
import processing.app.syntax.ArduinoTokenMakerFactory;
import processing.app.syntax.PdeKeywords;
import processing.app.syntax.SketchTextArea;
import processing.app.syntax.SketchTextAreaEditorKit;
import processing.app.tools.DiscourseFormat;

/**
 * Single tab, editing a single file, in the main window.
 */
public class EditorTab extends JPanel implements SketchFile.TextStorage, MouseWheelListener {
    protected Editor editor;
    protected SketchTextArea textarea;
    protected RTextScrollPane scrollPane;
    protected SketchFile file;
    protected boolean modified;
    /** Is external editing mode currently enabled? */
    protected boolean external;

    /**
     * Create a new EditorTab
     *
     * @param editor
     *          The Editor this tab runs in
     * @param file
     *          The file to display in this tab
     * @param contents
     *          Initial contents to display in this tab. Can be used when editing
     *          a file that doesn't exist yet. If null is passed, code.load() is
     *          called and displayed instead.
     * @throws IOException
     */
    public EditorTab(Editor editor, SketchFile file, String contents) throws IOException {
        super(new BorderLayout());

        // Load initial contents contents from file if nothing was specified.
        if (contents == null) {
            contents = file.load();
            modified = false;
        } else {
            modified = true;
        }

        this.editor = editor;
        this.file = file;
        RSyntaxDocument document = createDocument(contents);
        textarea = createTextArea(document);
        scrollPane = createScrollPane(textarea);
        file.setStorage(this);
        applyPreferences();
        add(scrollPane, BorderLayout.CENTER);
        textarea.addMouseWheelListener(this);
    }

    private RSyntaxDocument createDocument(String contents) {
        RSyntaxDocument document = new RSyntaxDocument(new ArduinoTokenMakerFactory(editor.base.getPdeKeywords()),
                RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS);
        document.putProperty(PlainDocument.tabSizeAttribute, PreferencesData.getInteger("editor.tabs.size"));

        // insert the program text into the document object
        try {
            document.insertString(0, contents, null);
        } catch (BadLocationException bl) {
            bl.printStackTrace();
        }
        document.addDocumentListener(new DocumentTextChangeListener(() -> setModified(true)));
        return document;
    }

    private RTextScrollPane createScrollPane(SketchTextArea textArea) throws IOException {
        RTextScrollPane scrollPane = new RTextScrollPane(textArea, true);
        scrollPane.setBorder(new MatteBorder(0, 6, 0, 0, Theme.getColor("editor.bgcolor")));
        scrollPane.setViewportBorder(BorderFactory.createEmptyBorder());
        scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers"));
        scrollPane.setIconRowHeaderEnabled(false);

        Gutter gutter = scrollPane.getGutter();
        gutter.setBookmarkingEnabled(false);
        //gutter.setBookmarkIcon(CompletionsRenderer.getIcon(CompletionType.TEMPLATE));
        gutter.setIconRowHeaderInheritsGutterBackground(true);

        return scrollPane;
    }

    private SketchTextArea createTextArea(RSyntaxDocument document) throws IOException {
        final SketchTextArea textArea = new SketchTextArea(document, editor.base.getPdeKeywords());
        textArea.setName("editor");
        textArea.setFocusTraversalKeysEnabled(false);
        //textArea.requestFocusInWindow();
        textArea.setMarkOccurrences(PreferencesData.getBoolean("editor.advanced"));
        textArea.setMarginLineEnabled(false);
        textArea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding"));
        textArea.setAutoIndentEnabled(PreferencesData.getBoolean("editor.indent"));
        textArea.setCloseCurlyBraces(PreferencesData.getBoolean("editor.auto_close_braces", true));
        textArea.setAntiAliasingEnabled(PreferencesData.getBoolean("editor.antialias"));
        textArea.setTabsEmulated(PreferencesData.getBoolean("editor.tabs.expand"));
        textArea.setTabSize(PreferencesData.getInteger("editor.tabs.size"));
        textArea.addHyperlinkListener(evt -> {
            try {
                UpdatableBoardsLibsFakeURLsHandler boardLibHandler = new UpdatableBoardsLibsFakeURLsHandler(
                        editor.base);
                boardLibHandler.openBoardLibManager(evt.getURL());
            } catch (Exception e) {
                try {
                    editor.platform.openURL(editor.getSketch().getFolder(), evt.getURL().toExternalForm());
                } catch (Exception f) {
                    Base.showWarning(f.getMessage(), f.getMessage(), f);
                }
            }
        });
        textArea.addCaretListener(e -> {
            Element root = textArea.getDocument().getDefaultRootElement();
            int lineStart = root.getElementIndex(e.getMark());
            int lineEnd = root.getElementIndex(e.getDot());

            editor.lineStatus.set(lineStart, lineEnd);
        });
        ToolTipManager.sharedInstance().registerComponent(textArea);

        configurePopupMenu(textArea);
        return textArea;
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
        if (e.isControlDown()) {
            if (e.getWheelRotation() < 0) {
                editor.base.handleFontSizeChange(1);
            } else {
                editor.base.handleFontSizeChange(-1);
            }
        } else {
            e.getComponent().getParent().dispatchEvent(e);
        }
    }

    private void configurePopupMenu(final SketchTextArea textarea) {

        JPopupMenu menu = textarea.getPopupMenu();

        menu.addSeparator();

        JMenuItem item = editor.createToolMenuItem("cc.arduino.packages.formatter.AStyle");
        if (item == null) {
            throw new NullPointerException("Tool cc.arduino.packages.formatter.AStyle unavailable");
        }
        item.setName("menuToolsAutoFormat");

        menu.add(item);

        item = new JMenuItem(tr("Comment/Uncomment"), '/');
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                handleCommentUncomment();
            }
        });
        menu.add(item);

        item = new JMenuItem(tr("Increase Indent"), ']');
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                handleIndentOutdent(true);
            }
        });
        menu.add(item);

        item = new JMenuItem(tr("Decrease Indent"), '[');
        item.setName("menuDecreaseIndent");
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                handleIndentOutdent(false);
            }
        });
        menu.add(item);

        item = new JMenuItem(tr("Copy for Forum"));
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                handleDiscourseCopy();
            }
        });
        menu.add(item);

        item = new JMenuItem(tr("Copy as HTML"));
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                handleHTMLCopy();
            }
        });
        menu.add(item);

        final JMenuItem referenceItem = new JMenuItem(tr("Find in Reference"));
        referenceItem.addActionListener(editor::handleFindReference);
        menu.add(referenceItem);

        final JMenuItem openURLItem = new JMenuItem(tr("Open URL"));
        openURLItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Base.openURL(e.getActionCommand());
            }
        });
        menu.add(openURLItem);

        menu.addPopupMenuListener(new PopupMenuListener() {

            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                String referenceFile = editor.base.getPdeKeywords().getReference(getCurrentKeyword());
                referenceItem.setEnabled(referenceFile != null);

                int offset = textarea.getCaretPosition();
                org.fife.ui.rsyntaxtextarea.Token token = RSyntaxUtilities.getTokenAtOffset(textarea, offset);
                if (token != null && token.isHyperlink()) {
                    openURLItem.setEnabled(true);
                    openURLItem.setActionCommand(token.getLexeme());
                } else {
                    openURLItem.setEnabled(false);
                }
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {
            }
        });

    }

    public void applyPreferences() {
        textarea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding"));
        scrollPane.setFoldIndicatorEnabled(PreferencesData.getBoolean("editor.code_folding"));
        scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers"));

        // apply the setting for 'use external editor', but only if it changed
        if (external != PreferencesData.getBoolean("editor.external")) {
            external = !external;
            if (external) {
                // disable line highlight and turn off the caret when disabling
                textarea.setBackground(Theme.getColor("editor.external.bgcolor"));
                textarea.setHighlightCurrentLine(false);
                textarea.setEditable(false);
                // Detach from the code, since we are no longer the authoritative source
                // for file contents.
                file.setStorage(null);
                // Reload, in case the file contents already changed.
                reload();
            } else {
                textarea.setBackground(Theme.getColor("editor.bgcolor"));
                textarea.setHighlightCurrentLine(Theme.getBoolean("editor.linehighlight"));
                textarea.setEditable(true);
                file.setStorage(this);
                // Reload once just before disabling external mode, to ensure we have
                // the latest contents.
                reload();
            }
        }
        // apply changes to the font size for the editor
        Font editorFont = scale(PreferencesData.getFont("editor.font"));

        // check whether a theme-defined editor font is available
        Font themeFont = Theme.getFont("editor.font");
        if (themeFont != null) {
            // Apply theme font if the editor font has *not* been changed by the user,
            // This allows themes to specify an editor font which will only be applied
            // if the user hasn't already changed their editor font via preferences.txt
            String defaultFontName = StringUtils.defaultIfEmpty(PreferencesData.getDefault("editor.font"), "")
                    .split(",")[0];
            if (defaultFontName.equals(editorFont.getName())) {
                editorFont = new Font(themeFont.getName(), themeFont.getStyle(), editorFont.getSize());
            }
        }

        textarea.setFont(editorFont);
        scrollPane.getGutter().setLineNumberFont(editorFont);
    }

    public void updateKeywords(PdeKeywords keywords) {
        // update GUI for "Find In Reference"
        textarea.setKeywords(keywords);
        // update document for syntax highlighting
        RSyntaxDocument document = (RSyntaxDocument) textarea.getDocument();
        document.setTokenMakerFactory(new ArduinoTokenMakerFactory(keywords));
        document.setSyntaxStyle(RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS);
    }

    /**
     * Called when this tab is made the current one, or when it is the current one
     * and the window is activated.
     */
    public void activated() {
        // When external editing is enabled, reload the text whenever we get activated.
        if (external) {
            reload();
        }
    }

    /**
     * Reload the contents of our file.
     */
    public void reload() {
        String text;
        try {
            text = file.load();
        } catch (IOException e) {
            System.err.println(I18n.format("Warning: Failed to reload file: \"{0}\"", file.getFileName()));
            return;
        }
        setText(text);
        setModified(false);
    }

    /**
     * Get the TextArea object for use (not recommended). This should only
     * be used in obscure cases that really need to hack the internals of the
     * JEditTextArea. Most tools should only interface via the get/set functions
     * found in this class. This will maintain compatibility with future releases,
     * which will not use TextArea.
     */
    public SketchTextArea getTextArea() {
        return textarea;
    }

    /**
     * Get the sketch this tab is editing a file from.
     */
    public SketchController getSketch() {
        return editor.getSketchController();
    }

    /**
     * Get the SketchFile that is being edited in this tab.
     */
    public SketchFile getSketchFile() {
        return this.file;
    }

    /**
     * Get the contents of the text area.
     */
    public String getText() {
        return textarea.getText();
    }

    /**
     * Replace the entire contents of this tab.
     */
    public void setText(String what) {
        // Remove all highlights, since these will all end up at the start of the
        // text otherwise. Preserving them is tricky, so better just remove them.
        textarea.removeAllLineHighlights();
        // Set the caret update policy to NEVER_UPDATE while completely replacing
        // the current text. Normally, the caret tracks inserts and deletions, but
        // replacing the entire text will always make the caret end up at the end,
        // which isn't really useful. With NEVER_UPDATE, the caret will just keep
        // its absolute position (number of characters from the start), which isn't
        // always perfect, but the best we can do without making a diff of the old
        // and new text and some guesswork.
        // Note that we cannot use textarea.setText() here, since that first removes
        // text and then inserts the new text. Even with NEVER_UPDATE, the caret
        // always makes sure to stay valid, so first removing all text makes it
        // reset to 0. Also note that simply saving and restoring the caret position
        // will work, but then the scroll position might change in response to the
        // caret position.
        DefaultCaret caret = (DefaultCaret) textarea.getCaret();
        int policy = caret.getUpdatePolicy();
        caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
        try {
            Document doc = textarea.getDocument();
            int oldLength = doc.getLength();
            // The undo manager already seems to group the insert and remove together
            // automatically, but better be explicit about it.
            textarea.beginAtomicEdit();
            try {
                doc.insertString(oldLength, what, null);
                doc.remove(0, oldLength);
            } catch (BadLocationException e) {
                System.err.println("Unexpected failure replacing text");
            } finally {
                textarea.endAtomicEdit();
            }
        } finally {
            caret.setUpdatePolicy(policy);
        }
    }

    /**
     * Is the text modified since the last save / load?
     */
    public boolean isModified() {
        return modified;
    }

    /**
     * Clear modified status. Should only be called by SketchFile through the
     * TextStorage interface.
     */
    public void clearModified() {
        setModified(false);
    }

    private void setModified(boolean value) {
        if (value != modified) {
            modified = value;
            // TODO: Improve decoupling
            editor.getSketchController().calcModified();
        }
    }

    public String getSelectedText() {
        return textarea.getSelectedText();
    }

    public void setSelectedText(String what) {
        textarea.replaceSelection(what);
    }

    public void setSelection(int start, int stop) {
        textarea.select(start, stop);
    }

    public int getScrollPosition() {
        return scrollPane.getVerticalScrollBar().getValue();
    }

    public void setScrollPosition(int pos) {
        scrollPane.getVerticalScrollBar().setValue(pos);
    }

    /**
     * Get the beginning point of the current selection.
     */
    public int getSelectionStart() {
        return textarea.getSelectionStart();
    }

    /**
     * Get the end point of the current selection.
     */
    public int getSelectionStop() {
        return textarea.getSelectionEnd();
    }

    /**
     * Get text for a specified line.
     */
    public String getLineText(int line) {
        try {
            return textarea.getText(textarea.getLineStartOffset(line), textarea.getLineEndOffset(line));
        } catch (BadLocationException e) {
            return "";
        }
    }

    /**
     * Jump to the given line
     * @param line The line number to jump to, 1-based. 
     */
    public void goToLine(int line) {
        if (line <= 0) {
            return;
        }
        try {
            textarea.setCaretPosition(textarea.getLineStartOffset(line - 1));
        } catch (BadLocationException e) {
            //ignore
        }
    }

    void handleCut() {
        textarea.cut();
    }

    void handleCopy() {
        textarea.copy();
    }

    void handlePaste() {
        textarea.paste();
    }

    void handleSelectAll() {
        textarea.selectAll();
    }

    void handleCommentUncomment() {
        Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaToggleCommentAction);
        action.actionPerformed(null);
        // XXX: RSyntaxDocument doesn't fire DocumentListener events, it should be fixed in RSyntaxTextArea?
        editor.updateUndoRedoState();
    }

    void handleDiscourseCopy() {
        new DiscourseFormat(editor, this, false).show();
    }

    void handleHTMLCopy() {
        new DiscourseFormat(editor, this, true).show();
    }

    void handleIndentOutdent(boolean indent) {
        if (indent) {
            Action action = textarea.getActionMap().get(SketchTextAreaEditorKit.rtaIncreaseIndentAction);
            action.actionPerformed(null);
        } else {
            Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaDecreaseIndentAction);
            action.actionPerformed(null);
        }
        // XXX: RSyntaxDocument doesn't fire DocumentListener events, it should be fixed in RSyntaxTextArea?
        editor.updateUndoRedoState();
    }

    void handleUndo() {
        textarea.undoLastAction();
    }

    void handleRedo() {
        textarea.redoLastAction();
    }

    public String getCurrentKeyword() {
        String text = "";
        if (textarea.getSelectedText() != null)
            text = textarea.getSelectedText().trim();

        try {
            int current = textarea.getCaretPosition();
            int startOffset = 0;
            int endIndex = current;
            String tmp = textarea.getDocument().getText(current, 1);
            // TODO probably a regexp that matches Arduino lang special chars
            // already exists.
            String regexp = "[\\s\\n();\\\\.!='\\[\\]{}]";

            while (!tmp.matches(regexp)) {
                endIndex++;
                tmp = textarea.getDocument().getText(endIndex, 1);
            }
            // For some reason document index start at 2.
            // if( current - start < 2 ) return;

            tmp = "";
            while (!tmp.matches(regexp)) {
                startOffset++;
                if (current - startOffset < 0) {
                    tmp = textarea.getDocument().getText(0, 1);
                    break;
                } else
                    tmp = textarea.getDocument().getText(current - startOffset, 1);
            }
            startOffset--;

            int length = endIndex - current + startOffset;
            text = textarea.getDocument().getText(current - startOffset, length);

        } catch (BadLocationException bl) {
            bl.printStackTrace();
        }
        return text;
    }

    @Override
    public boolean requestFocusInWindow() {
        /** If focus is requested, focus the textarea instead. */
        return textarea.requestFocusInWindow();
    }

}