Java tutorial
// Copyright (c) 2012 Chan Wai Shing // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package radsoft.syntaxhighlighter.view; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JTextPane; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.BoxView; import javax.swing.text.ComponentView; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.Element; import javax.swing.text.Highlighter; import javax.swing.text.IconView; import javax.swing.text.JTextComponent; import javax.swing.text.LabelView; import javax.swing.text.ParagraphView; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledEditorKit; import javax.swing.text.View; import javax.swing.text.ViewFactory; import radsoft.syntaxhighlighter.view.theme.Theme; import com.google.common.collect.Range; /** * The text pane for displaying the script text. * @author Chan Wai Shing <cws1989@gmail.com> */ public class SyntaxHighlighterPane extends JTextPane { private static final Logger LOG = Logger.getLogger(SyntaxHighlighterPane.class.getName()); private static final long serialVersionUID = 1L; /** * The line number offset. E.g. set offset to 9 will make the first line * number to appear at line 1 + 9 = 10 */ private int lineNumberOffset; /** * The background color of the highlighted line. Default is black. */ private Color highlightedBackground; /** * Indicator that indicate to turn on the mouse-over highlight effect or not. * See {@link #setHighlightOnMouseOver(boolean)}. */ private boolean highlightWhenMouseOver; /** * The list of line numbers that indicate which lines are needed to be * highlighted. */ protected final List<Integer> highlightedLineList; /** * The highlighter painter used to do the highlight line effect. */ protected Highlighter.HighlightPainter highlightPainter; /** * The theme. */ protected Theme theme; /** * The style list. */ protected Map<String, List<Range<Integer>>> styleList; /** * Record the mouse cursor is currently pointing at which line of the * document. -1 means not any line. * It is used internally. */ protected int mouseOnLine; /** * Constructor. */ public SyntaxHighlighterPane() { super(); setEditable(false); //<editor-fold defaultstate="collapsed" desc="editor kit"> setEditorKit(new StyledEditorKit() { private static final long serialVersionUID = 1L; @Override public ViewFactory getViewFactory() { return new ViewFactory() { @Override public View create(Element elem) { String kind = elem.getName(); if (kind != null) { if (kind.equals(AbstractDocument.ContentElementName)) { return new LabelView(elem) { @Override public int getBreakWeight(int axis, float pos, float len) { return 0; } }; } else if (kind.equals(AbstractDocument.ParagraphElementName)) { return new ParagraphView(elem) { @Override public int getBreakWeight(int axis, float pos, float len) { return 0; } }; } else if (kind.equals(AbstractDocument.SectionElementName)) { return new BoxView(elem, View.Y_AXIS); } else if (kind.equals(StyleConstants.ComponentElementName)) { return new ComponentView(elem) { @Override public int getBreakWeight(int axis, float pos, float len) { return 0; } }; } else if (kind.equals(StyleConstants.IconElementName)) { return new IconView(elem); } } return new LabelView(elem) { @Override public int getBreakWeight(int axis, float pos, float len) { return 0; } }; } }; } }); //</editor-fold> lineNumberOffset = 0; //<editor-fold defaultstate="collapsed" desc="highlighter painter"> highlightedBackground = Color.black; highlightWhenMouseOver = true; highlightedLineList = new ArrayList<Integer>(); highlightPainter = new Highlighter.HighlightPainter() { @Override public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) { if (c.getParent() == null) { return; } // get the Y-axis value of the visible area of the text component int startY = Math.abs(c.getY()); int endY = startY + c.getParent().getHeight(); FontMetrics textPaneFontMetrics = g.getFontMetrics(getFont()); int textPaneFontHeight = textPaneFontMetrics.getHeight(); int largerestLineNumber = c.getDocument().getDefaultRootElement().getElementCount(); g.setColor(highlightedBackground); // draw the highlight background to the highlighted line synchronized (highlightedLineList) { for (Integer lineNumber : highlightedLineList) { if (lineNumber > largerestLineNumber + lineNumberOffset) { // skip those line number that out of range continue; } // get the Y-axis value of this highlighted line int _y = Math.max(0, textPaneFontHeight * (lineNumber - lineNumberOffset - 1)); if (_y > endY || _y + textPaneFontHeight < startY) { // this line is out of visible area, skip it continue; } // draw the highlighted background g.fillRect(0, _y, c.getWidth(), textPaneFontHeight); } } // draw the mouse-over-highlight effect if (mouseOnLine != -1) { if (mouseOnLine <= largerestLineNumber + lineNumberOffset) { int _y = Math.max(0, textPaneFontHeight * (mouseOnLine - lineNumberOffset - 1)); if (_y < endY && _y + textPaneFontHeight > startY) { // the line is within the range of visible area g.fillRect(0, _y, c.getWidth(), textPaneFontHeight); } } } } }; try { getHighlighter().addHighlight(0, 0, highlightPainter); } catch (BadLocationException ex) { LOG.log(Level.SEVERE, null, ex); } //</editor-fold> mouseOnLine = -1; //<editor-fold defaultstate="collapsed" desc="mouse listener"> addMouseListener(new MouseAdapter() { @Override public void mouseExited(MouseEvent e) { if (!highlightWhenMouseOver) { return; } mouseOnLine = -1; repaint(); } }); addMouseMotionListener(new MouseMotionListener() { @Override public void mouseDragged(MouseEvent e) { } @Override public void mouseMoved(MouseEvent e) { if (!highlightWhenMouseOver) { return; } Element defaultRootElement = getDocument().getDefaultRootElement(); // get the position of the document the mouse cursor is pointing int documentOffsetStart = viewToModel(e.getPoint()); // the line number that the mouse cursor is currently pointing int lineNumber = documentOffsetStart == -1 ? -1 : defaultRootElement.getElementIndex(documentOffsetStart) + 1 + lineNumberOffset; if (lineNumber == defaultRootElement.getElementCount()) { // if the line number got is the last line, check if the cursor is actually on the line or already below the line try { Rectangle rectangle = modelToView(documentOffsetStart); if (e.getY() > rectangle.y + rectangle.height) { lineNumber = -1; } } catch (BadLocationException ex) { LOG.log(Level.SEVERE, null, ex); } } // only repaint when the line number changed if (mouseOnLine != lineNumber) { mouseOnLine = lineNumber; repaint(); } } }); //</editor-fold> } @Override public void setHighlighter(Highlighter highlighter) { if (highlightPainter != null) { getHighlighter().removeHighlight(highlightPainter); try { highlighter.addHighlight(0, 0, highlightPainter); } catch (BadLocationException ex) { LOG.log(Level.SEVERE, null, ex); } } super.setHighlighter(highlighter); } /** * Set the content of the syntax highlighter. It is better to set other * settings first and set this the last. * * @param content the content to set */ public void setContent(String content) { String newContent = content == null ? "" : content; DefaultStyledDocument document = (DefaultStyledDocument) getDocument(); try { document.remove(0, document.getLength()); if (theme != null) { document.insertString(0, newContent, theme.getPlain().getAttributeSet()); } else { document.insertString(0, newContent, new SimpleAttributeSet()); } } catch (BadLocationException ex) { LOG.log(Level.SEVERE, null, ex); } setCaretPosition(0); // clear the style list styleList = null; } public String getContent() { DefaultStyledDocument document = (DefaultStyledDocument) getDocument(); try { return document.getText(0, document.getLength()); } catch (BadLocationException ex) { LOG.log(Level.SEVERE, null, ex); return null; } } public void setStyle(Map<Range<Integer>, String> styleList) { if (styleList == null) { throw new NullPointerException("argumenst 'styleList' cannot be null"); } this.styleList = new HashMap<String, List<Range<Integer>>>(); for (Map.Entry<Range<Integer>, String> parseResult : styleList.entrySet()) { String styleKeysString = parseResult.getValue(); List<Range<Integer>> _styleList = this.styleList.get(styleKeysString); if (_styleList == null) { _styleList = new ArrayList<Range<Integer>>(); this.styleList.put(styleKeysString, _styleList); } _styleList.add(parseResult.getKey()); } applyStyle(); } /** * Apply the list of style to the script text pane. See * {@link syntaxhighlighter.parser.Parser#parse(syntaxhighlighter.brush.Brush, boolean, char[], int, int)}. */ protected void applyStyle() { if (theme == null || styleList == null) { return; } DefaultStyledDocument document = (DefaultStyledDocument) getDocument(); // clear all the existing style document.setCharacterAttributes(0, document.getLength(), theme.getPlain().getAttributeSet(), true); // apply style according to the style list for (String key : styleList.keySet()) { List<Range<Integer>> posList = styleList.get(key); SimpleAttributeSet attributeSet = theme.getStyle(key).getAttributeSet(); for (Range<Integer> pos : posList) { int start = pos.lowerEndpoint(); int end = pos.upperEndpoint(); document.setCharacterAttributes(start, end - start, attributeSet, true); } } repaint(); } /** * Get current theme. * * @return the current theme */ public Theme getTheme() { return theme; } /** * Set the theme. * * @param theme the theme */ public void setTheme(Theme theme) { if (theme == null) { throw new NullPointerException("argument 'theme' cannot be null"); } this.theme = theme; setFont(theme.getFont()); setBackground(theme.getBackground()); setHighlightedBackground(theme.getHighlightedBackground()); if (styleList != null) { applyStyle(); } } /** * Get the line number offset. E.g. set offset to 9 will make the first line * number to appear at line 1 + 9 = 10 * * @return the offset */ public int getLineNumberOffset() { return lineNumberOffset; } /** * Set the line number offset. E.g. set offset to 9 will make the first line * number to appear at line 1 + 9 = 10 * * @param offset the offset */ public void setLineNumberOffset(int offset) { lineNumberOffset = Math.max(0, offset); repaint(); } /** * Get the color of the highlighted background. Default is black. * * @return the color */ public Color getHighlightedBackground() { return highlightedBackground; } /** * Set the color of the highlighted background. Default is black. * * @param highlightedBackground the color */ public void setHighlightedBackground(Color highlightedBackground) { if (highlightedBackground == null) { throw new NullPointerException("argument 'highlightedBackground' cannot be null"); } this.highlightedBackground = highlightedBackground; repaint(); } /** * Check the status of the mouse-over highlight effect. Default is on. * * @return true if turned on, false if turned off */ public boolean isHighlightOnMouseOver() { return highlightWhenMouseOver; } /** * Set turn on the mouse-over highlight effect or not. Default is on. * If set true, there will be a highlight effect on the line that the mouse * cursor currently is pointing on (on the script text area only, not on the * line number panel). * * @param highlightWhenMouseOver true to turn on, false to turn off */ public void setHighlightOnMouseOver(boolean highlightWhenMouseOver) { this.highlightWhenMouseOver = highlightWhenMouseOver; if (!highlightWhenMouseOver) { mouseOnLine = -1; } repaint(); } /** * Get the list of highlighted lines. * * @return a copy of the list */ public List<Integer> getHighlightedLineList() { return new ArrayList<Integer>(highlightedLineList); } /** * Set highlighted lines. Note that this will clear all previous recorded * highlighted lines. * * @param highlightedLineList the list that contain the highlighted lines */ public void setHighlightedLineList(List<Integer> highlightedLineList) { if (highlightedLineList == null) { throw new NullPointerException("argument 'highlightedLineList' cannot be null"); } synchronized (this.highlightedLineList) { this.highlightedLineList.clear(); this.highlightedLineList.addAll(highlightedLineList); } repaint(); } /** * Add highlighted line. * * @param lineNumber the line number to highlight */ public void addHighlightedLine(int lineNumber) { highlightedLineList.add(lineNumber); repaint(); } /** * Set the {@code font} according to {@code bold} and {@code italic}. * * @param font the font to set * @param bold true to set bold, false not * @param italic true to set italic, false not * * @return the font with bold and italic changed, or null if the input * {@code font} is null */ protected static Font setFont(Font font, boolean bold, boolean italic) { if (font == null) { return null; } if ((font.getStyle() & Font.BOLD) != 0) { if (!bold) { return font.deriveFont(font.getStyle() ^ Font.BOLD); } } else { if (bold) { return font.deriveFont(font.getStyle() | Font.BOLD); } } if ((font.getStyle() & Font.ITALIC) != 0) { if (!italic) { return font.deriveFont(font.getStyle() ^ Font.ITALIC); } } else { if (italic) { return font.deriveFont(font.getStyle() | Font.ITALIC); } } return font; } }