net.team2xh.crt.gui.editor.EditorTextPane.java Source code

Java tutorial

Introduction

Here is the source code for net.team2xh.crt.gui.editor.EditorTextPane.java

Source

/*
 * Copyright (C) 2015 Hamza Haiken <tenchi@team2xh.net>
 *
 * This program 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.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */
package net.team2xh.crt.gui.editor;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.DocumentFilter;
import javax.swing.text.Element;
import javax.swing.text.LayeredHighlighter;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import net.team2xh.crt.gui.themes.Theme;
import net.team2xh.crt.language.parser.CRTBaseListener;
import net.team2xh.crt.language.parser.CRTLexer;
import net.team2xh.crt.language.parser.CRTParser;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.NotNull;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.apache.commons.lang3.StringEscapeUtils;

/**
 * The actual editor.
 *
 * @author Hamza Haiken <tenchi@team2xh.net>
 */
public class EditorTextPane extends JTextPane {

    private DefaultStyledDocument doc;

    private boolean changed = false;

    //    private int length = 0;
    //    private int cursor = 0;
    //    private int line = 0;
    final private LineHighlighter lh;
    final private LineHighlighter ll;
    final private WordHighlighter wh;
    final private ErrorHighlighter eh;

    final private Font font = new Font("Envy Code R", Font.PLAIN, 16);
    final private FontMetrics fontMetrics = getFontMetrics(font);
    final private int charWidth = fontMetrics.charWidth('0');

    private int margin = 60;
    private int marginSize = margin * charWidth + 3;

    private LinkedList<Object> occurrences = new LinkedList<>();

    public EditorTextPane() {

        // Initialize highlighters
        lh = new LineHighlighter(Theme.getTheme().COLOR_13);
        ll = new LineHighlighter(Theme.getTheme().COLOR_14);
        wh = new WordHighlighter(Theme.getTheme().COLOR_11);
        eh = new ErrorHighlighter(Color.RED);

        // Initialise colors
        initAttributeSets();

        setOpaque(false); // Background will be drawn later on
        setFont(font);

        doc = (DefaultStyledDocument) getDocument();

        // Replace all tabs with four spaces
        // TODO: tab to next multiple of 4 column
        // TODO: tab whole selection
        // TODO: insert matching brace
        doc.setDocumentFilter(new DocumentFilter() {
            private String process(int offset, String text) {
                return text.replaceAll("\t", "    ").replaceAll("\r", "");
            }

            @Override
            public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr)
                    throws BadLocationException {
                super.insertString(fb, offset, process(offset, text), attr);
            }

            @Override
            public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attr)
                    throws BadLocationException {
                super.replace(fb, offset, length, process(offset, text), attr);
            }
        });

        // Highlight text when text changes
        doc.addDocumentListener(new DocumentListener() {

            @Override
            public void removeUpdate(DocumentEvent e) {
                SwingUtilities.invokeLater(() -> highlightText());
                changed = true;
            }

            @Override
            public void insertUpdate(DocumentEvent e) {
                SwingUtilities.invokeLater(() -> highlightText());
                changed = true;
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
            }
        });

        addCaretListener(new CaretListener() {
            private boolean isAlphanum(char c) {
                return Character.isDigit(c) || Character.isLetter(c);
            }

            private boolean isAlphanum(String s) {
                for (char c : s.toCharArray()) // Allow dots in middle of words for floats
                {
                    if (c != '.' && !isAlphanum(c)) {
                        return false;
                    }
                }

                return true;
            }

            @Override
            public void caretUpdate(CaretEvent ce) {
                try {

                    // Highlight current line
                    highlightCurrentLine();

                    // Clear previously highlighted occurrences
                    for (Object o : occurrences) {
                        getHighlighter().removeHighlight(o);
                    }
                    repaint();
                    occurrences.clear();

                    // Get start and end offsets, swap them if necessary
                    int s = ce.getDot();
                    int e = ce.getMark();

                    if (s > e) {
                        s = s + e;
                        e = s - e;
                        s = s - e;
                    }

                    // If there is a selection,
                    if (s != e) {
                        // Check if the char on the left and on the right are not alphanums
                        char f = s == 0 ? ' ' : doc.getText(s - 1, 1).charAt(0);
                        char l = s == doc.getLength() - 1 ? ' ' : doc.getText(e, 1).charAt(0);
                        if (!isAlphanum(f) && !isAlphanum(l)) {
                            String word = doc.getText(s, e - s);
                            if (isAlphanum(word)) {
                                highlightOccurrences(word, s, e);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        setCaretColor(Theme.getTheme().COLOR_11);

        highlightText();
    }

    private void highlightCurrentLine() {
        if (lastLine != null) {
            getHighlighter().removeHighlight(lastLine);
        }
        try {
            int line = getLineOfOffset(getCaretPosition());
            int ss = getLineStartOffset(line);
            int ee = getLineEndOffset(line);
            lastLine = highlight(ss, ee, ll);
        } catch (BadLocationException ex) {
            Logger.getLogger(EditorTextPane.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    class SyntaxHighlightingListener extends CRTBaseListener {

        @Override
        public void visitErrorNode(@NotNull ErrorNode node) {

            Token t = node.getSymbol();
            String m = node.getText();
            System.out.println("Parse error: " + m);
            try {
                int line = t.getLine() - 1;
                int s = getLineStartOffset(line);
                int e = getLineEndOffset(line);
                int o = t.getCharPositionInLine();
                int l = m.length();
                // TODO: Don't hide under current line highlight
                highlight(s + o, s + o + l, eh);
                highlight(s, e, lh);
            } catch (BadLocationException e) {
                e.printStackTrace();
            }
        }
    }

    private void colorize(SimpleAttributeSet style, int line, int cursor, int length) {
        try {
            int index = getLineStartOffset(line) + cursor;
            doc.setCharacterAttributes(index, length, style, true);
        } catch (BadLocationException ex) {
            Logger.getLogger(EditorTextPane.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private Object highlight(int start, int end, LayeredHighlighter.LayerPainter hl) throws BadLocationException {
        return getHighlighter().addHighlight(start, end, hl);
    }

    private SimpleAttributeSet OPERATORS = new SimpleAttributeSet();
    private SimpleAttributeSet NAME = new SimpleAttributeSet();
    private SimpleAttributeSet IDENTIFIER = new SimpleAttributeSet();
    private SimpleAttributeSet NUMBER = new SimpleAttributeSet();
    private SimpleAttributeSet COMMENT = new SimpleAttributeSet();
    private SimpleAttributeSet BLOCK = new SimpleAttributeSet();
    private SimpleAttributeSet NORMAL = new SimpleAttributeSet();
    private SimpleAttributeSet TRANSFORM = new SimpleAttributeSet();
    private SimpleAttributeSet STRING = new SimpleAttributeSet();

    private void initAttributeSets() {
        Theme theme = Theme.getTheme();
        StyleConstants.setForeground(OPERATORS, theme.COLOR_10);
        StyleConstants.setForeground(NAME, theme.COLOR_11);
        StyleConstants.setItalic(NAME, true);
        StyleConstants.setForeground(NUMBER, theme.COLOR_09);
        StyleConstants.setForeground(COMMENT, theme.COLOR_16);
        StyleConstants.setForeground(BLOCK, theme.COLOR_06);
        StyleConstants.setItalic(BLOCK, true);
        StyleConstants.setForeground(NORMAL, theme.COLOR_11);
        StyleConstants.setForeground(IDENTIFIER, theme.COLOR_11);
        StyleConstants.setForeground(TRANSFORM, theme.COLOR_08);
        StyleConstants.setForeground(STRING, theme.COLOR_12);
    }

    private Object lastLine = null;

    private void highlightText() {

        getHighlighter().removeAllHighlights();
        colorize(COMMENT, 0, 0, getText().length());

        highlightCurrentLine();

        CRTLexer lexer = new CRTLexer(new ANTLRInputStream(getText()));
        CRTParser parser = new CRTParser(new CommonTokenStream(lexer));
        ParserRuleContext tree = parser.script();
        ParseTreeWalker walker = new ParseTreeWalker();

        lexer = new CRTLexer(new ANTLRInputStream(getText()));

        for (Token token = lexer.nextToken(); token.getType() != Token.EOF; token = lexer.nextToken()) {
            int cursor = token.getCharPositionInLine();
            int line = token.getLine() - 1;
            int length = token.getText().length();

            switch (token.getType()) {
            case CRTLexer.ASSIGN:
            case CRTLexer.ATTRIBUTE:
            case CRTLexer.ADD:
            case CRTLexer.SUBTRACT:
            case CRTLexer.INTERSECTION:
            case CRTLexer.MULTIPLY:
            case CRTLexer.DIVIDE:
            case CRTLexer.MODULO:
            case CRTLexer.NOT:
            case CRTLexer.LESS:
            case CRTLexer.GREATER:
            case CRTLexer.LESS_EQUAL:
            case CRTLexer.GREATER_EQUAL:
            case CRTLexer.EQUAL:
            case CRTLexer.NOT_EQUAL:
            case CRTLexer.AND:
            case CRTLexer.OR:
            case CRTLexer.QUESTION:
            case CRTLexer.COLON:
                colorize(OPERATORS, line, cursor, length);
                break;

            case CRTLexer.TRANSLATE:
            case CRTLexer.SCALE:
            case CRTLexer.ROTATE:
                colorize(TRANSFORM, line, cursor, length);
                break;

            case CRTLexer.NAME:
                colorize(NAME, line, cursor, length);
                break;

            case CRTLexer.IDENTIFIER:
                colorize(IDENTIFIER, line, cursor, length);
                break;

            case CRTLexer.FLOAT:
            case CRTLexer.INTEGER:
            case CRTLexer.TRUE:
            case CRTLexer.FALSE:
                colorize(NUMBER, line, cursor, length);
                break;

            case CRTLexer.SCENE:
            case CRTLexer.SETTINGS:
            case CRTLexer.MACRO:
                colorize(BLOCK, line, cursor, length);
                break;

            case CRTLexer.STRING:
                colorize(STRING, line, cursor, length);
                break;

            // TODO: don't hide under line highlight
            case CRTLexer.INVALID:
                try {
                    int index = getLineStartOffset(line) + cursor;
                    int s = getLineStartOffset(index);
                    int e = getLineEndOffset(index);
                    highlight(cursor, cursor + length, eh);
                    highlight(s, e, lh);
                } catch (BadLocationException e) {
                }
                break;

            default:
                colorize(NORMAL, line, cursor, length);
                break;
            }
        }

        // Reset lexer for errors because parser consumed it
        walker.walk(new SyntaxHighlightingListener(), tree);

    }

    private void highlightOccurrences(String word, int s0, int e0) {
        try {
            Pattern p = Pattern.compile("\\b" + word + "\\b"); // word surrounded by boundaries
            Matcher m = p.matcher(doc.getText(0, doc.getLength()));
            while (m.find()) {
                int s = m.start();
                int e = m.end();
                if (s != s0 && e != e0) {
                    occurrences.add(highlight(s, e, wh));
                }
            }
        } catch (Exception e) {

        }
    }

    /**
     * Paints the background, the margin background and the margin line.
     *
     * @param g
     */
    @Override
    public void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        int m = marginSize;
        int w = getWidth(), h = getHeight();
        // Background
        g2d.setPaint(Theme.getTheme().COLOR_02);
        g2d.fillRect(0, 0, m, h);
        // Margin background
        if (m < w) {
            g2d.setColor(Theme.getTheme().COLOR_01);
            g2d.fillRect(m, 0, w - m, h);
        }
        // Margin line
        g2d.setColor(Theme.getTheme().COLOR_04);
        g2d.drawLine(m, 0, m, h);
        // Draw the rest
        super.paintComponent(g);
    }

    /**
     * Returns the first character offset of the given line number.
     *
     * @param line Line number.
     * @return First character offset of that line.
     * @throws BadLocationException
     */
    public int getLineStartOffset(int line) throws BadLocationException {
        Element map = doc.getDefaultRootElement();
        if (line < 0) {
            throw new BadLocationException("Negative line", -1);
        } else if (line >= map.getElementCount()) {
            throw new BadLocationException("No such line", doc.getLength() + 1);
        } else {
            Element lineElem = map.getElement(line);
            return lineElem.getStartOffset();
        }
    }

    /**
     * Returns the last character offset of the given line number.
     *
     * @param line Line number.
     * @return Last character offset of that line.
     * @throws BadLocationException
     */
    public int getLineEndOffset(int line) throws BadLocationException {
        Element map = doc.getDefaultRootElement();
        if (line < 0) {
            throw new BadLocationException("Negative line", -1);
        } else if (line >= map.getElementCount()) {
            throw new BadLocationException("No such line", doc.getLength() + 1);
        } else {
            Element lineElem = map.getElement(line);
            return lineElem.getEndOffset();
        }
    }

    /**
     * Returns the line number of the given character offset, for the current document.
     *
     * @param offset Character offset.
     * @return Line number.
     * @throws BadLocationException
     */
    public int getLineOfOffset(int offset) throws BadLocationException {
        if (offset < 0) {
            throw new BadLocationException("Can't translate offset to line", -1);
        } else if (offset > doc.getLength()) {
            throw new BadLocationException("Can't translate offset to line", doc.getLength() + 1);
        } else {
            Element map = doc.getDefaultRootElement();
            return map.getElementIndex(offset);
        }
    }

    public boolean getChanged() {
        return changed;
    }

    public void resetChanged() {
        changed = false;
    }
}