| // 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 syntaxhighlight; | |
| 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; | |
| /** | |
| * 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<ParseResult>> 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 void setStyle(List<ParseResult> styleList) { | |
| if (styleList == null) { | |
| throw new NullPointerException("argumenst 'styleList' cannot be null"); | |
| } | |
| this.styleList = new HashMap<String, List<ParseResult>>(); | |
| for (ParseResult parseResult : styleList) { | |
| String styleKeysString = parseResult.getStyleKeysString(); | |
| List<ParseResult> _styleList = this.styleList.get(styleKeysString); | |
| if (_styleList == null) { | |
| _styleList = new ArrayList<ParseResult>(); | |
| this.styleList.put(styleKeysString, _styleList); | |
| } | |
| _styleList.add(parseResult); | |
| } | |
| 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<ParseResult> posList = styleList.get(key); | |
| SimpleAttributeSet attributeSet = theme.getStyle(key).getAttributeSet(); | |
| for (ParseResult pos : posList) { | |
| document.setCharacterAttributes(pos.getOffset(), pos.getLength(), 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; | |
| } | |
| } |