/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* * TextAreaPainter.java - Paints the text area * Copyright (C) 1999 Slava Pestov * * You may use and modify this package for any purpose. Redistribution is * permitted, in both source and binary form, provided that this notice * remains intact in all source distributions of this package. */ package processing.app.syntax; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.MouseEvent; import javax.swing.ToolTipManager; import javax.swing.text.*; import javax.swing.JComponent; import processing.app.Preferences; import processing.app.syntax.im.CompositionTextPainter; import processing.app.ui.Toolkit; /** * The text area repaint manager. It performs double buffering and paints * lines of text. * @author Slava Pestov */ public class TextAreaPainter extends JComponent implements TabExpander { /** A specific painter composed by the InputMethod.*/ protected CompositionTextPainter compositionTextPainter; protected JEditTextArea textArea; protected TextAreaDefaults defaults; // protected boolean blockCaret; // protected SyntaxStyle[] styles; // protected Color caretColor; // protected Color selectionColor; // protected Color lineHighlightColor; // protected boolean lineHighlight; // protected Color bracketHighlightColor; // protected boolean bracketHighlight; // protected Color eolMarkerColor; // protected boolean eolMarkers; // protected int cols; // protected int rows; // moved from TextAreaDefaults private Font plainFont; private Font boldFont; private boolean antialias; // private Color fgcolor; // private Color bgcolor; protected int tabSize; protected FontMetrics fm; protected Highlight highlights; int currentLineIndex; Token currentLineTokens; Segment currentLine; /** * Creates a new repaint manager. This should be not be called directly. */ public TextAreaPainter(JEditTextArea textArea, TextAreaDefaults defaults) { this.textArea = textArea; this.defaults = defaults; setAutoscrolls(true); // setDoubleBuffered(true); setOpaque(true); ToolTipManager.sharedInstance().registerComponent(this); currentLine = new Segment(); currentLineIndex = -1; setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); // // unfortunately probably can't just do setDefaults() since things aren't quite set up // setFont(defaults.plainFont); //// System.out.println("defaults font is " + defaults.font); // setForeground(defaults.fgcolor); // setBackground(defaults.bgcolor); updateAppearance(); // blockCaret = defaults.blockCaret; // styles = defaults.styles; // caretColor = defaults.caretColor; // selectionColor = defaults.selectionColor; // lineHighlightColor = defaults.lineHighlightColor; // lineHighlight = defaults.lineHighlight; // bracketHighlightColor = defaults.bracketHighlightColor; // bracketHighlight = defaults.bracketHighlight; // eolMarkerColor = defaults.eolMarkerColor; // eolMarkers = defaults.eolMarkers; // antialias = defaults.antialias; // cols = defaults.cols; // rows = defaults.rows; } public void updateAppearance() { setForeground(defaults.fgcolor); setBackground(defaults.bgcolor); // Ensure that our monospaced font is loaded // https://github.com/processing/processing/pull/4639 Toolkit.getMonoFontName(); String fontFamily = Preferences.get("editor.font.family"); final int fontSize = Toolkit.zoom(Preferences.getInteger("editor.font.size")); plainFont = new Font(fontFamily, Font.PLAIN, fontSize); if (!fontFamily.equals(plainFont.getFamily())) { System.err.println(fontFamily + " not available, resetting to monospaced"); fontFamily = "Monospaced"; Preferences.set("editor.font.family", fontFamily); plainFont = new Font(fontFamily, Font.PLAIN, fontSize); } boldFont = new Font(fontFamily, Font.BOLD, fontSize); antialias = Preferences.getBoolean("editor.smooth"); // moved from setFont() override (never quite comfortable w/ that override) fm = super.getFontMetrics(plainFont); tabSize = fm.charWidth(' ') * Preferences.getInteger("editor.tabs.size"); textArea.recalculateVisibleLines(); } /* public void setDefaults(TextAreaDefaults defaults) { setFont(defaults.font); setForeground(defaults.fgcolor); setBackground(defaults.bgcolor); setBlockCaretEnabled(defaults.blockCaret); setStyles(defaults.styles); setCaretColor(defaults.caretColor); setSelectionColor(defaults.selectionColor); setLineHighlightColor(defaults.lineHighlightColor); setLineHighlightEnabled(defaults.lineHighlight); setBracketHighlightColor(defaults.bracketHighlightColor); setBracketHighlightEnabled(defaults.bracketHighlight); setEOLMarkerColor(defaults.eolMarkerColor); setEOLMarkersPainted(defaults.eolMarkers); setAntialias(defaults.antialias); // only used for getPreferredSize() cols = defaults.cols; rows = defaults.rows; } */ /** * Get CompositionTextPainter, creating one if it doesn't exist. */ public CompositionTextPainter getCompositionTextpainter() { if (compositionTextPainter == null){ compositionTextPainter = new CompositionTextPainter(textArea); } return compositionTextPainter; } /** * Returns the syntax styles used to paint colorized text. Entry <i>n</i> * will be used to paint tokens with id = <i>n</i>. * @see processing.app.syntax.Token */ public final SyntaxStyle[] getStyles() { return defaults.styles; } // /** // * Sets the syntax styles used to paint colorized text. Entry <i>n</i> // * will be used to paint tokens with id = <i>n</i>. // * @param styles The syntax styles // * @see processing.app.syntax.Token // */ // public final void setStyles(SyntaxStyle[] styles) { // this.styles = styles; // repaint(); // } // /** // * Returns the caret color. // */ // public final Color getCaretColor() { // return caretColor; // } // /** // * Sets the caret color. // * @param caretColor The caret color // */ // public final void setCaretColor(Color caretColor) { // this.caretColor = caretColor; // invalidateSelectedLines(); // } // /** // * Returns the selection color. // */ // public final Color getSelectionColor() { // return selectionColor; // } // /** // * Sets the selection color. // * @param selectionColor The selection color // */ // public final void setSelectionColor(Color selectionColor) { // this.selectionColor = selectionColor; // invalidateSelectedLines(); // } // /** // * Returns the line highlight color. // */ // public final Color getLineHighlightColor() { // return lineHighlightColor; // } // /** // * Sets the line highlight color. // * @param lineHighlightColor The line highlight color // */ // public final void setLineHighlightColor(Color lineHighlightColor) { // this.lineHighlightColor = lineHighlightColor; // invalidateSelectedLines(); // } // /** // * Returns true if line highlight is enabled, false otherwise. // */ // public final boolean isLineHighlightEnabled() { // return lineHighlight; // } /** * Enables or disables current line highlighting. * @param lineHighlight True if current line highlight * should be enabled, false otherwise */ public final void setLineHighlightEnabled(boolean lineHighlight) { // this.lineHighlight = lineHighlight; defaults.lineHighlight = lineHighlight; invalidateSelectedLines(); } // /** // * Returns the bracket highlight color. // */ // public final Color getBracketHighlightColor() { // return bracketHighlightColor; // } // /** // * Sets the bracket highlight color. // * @param bracketHighlightColor The bracket highlight color // */ // public final void setBracketHighlightColor(Color bracketHighlightColor) { // this.bracketHighlightColor = bracketHighlightColor; // invalidateLine(textArea.getBracketLine()); // } /** * Returns true if bracket highlighting is enabled, false otherwise. * When bracket highlighting is enabled, the bracket matching the * one before the caret (if any) is highlighted. */ public final boolean isBracketHighlightEnabled() { // return bracketHighlight; return defaults.bracketHighlight; } // /** // * Enables or disables bracket highlighting. // * When bracket highlighting is enabled, the bracket matching the // * one before the caret (if any) is highlighted. // * @param bracketHighlight True if bracket highlighting should be // * enabled, false otherwise // */ // public final void setBracketHighlightEnabled(boolean bracketHighlight) { // this.bracketHighlight = bracketHighlight; // invalidateLine(textArea.getBracketLine()); // } /** * Returns true if the caret should be drawn as a block, false otherwise. */ public final boolean isBlockCaretEnabled() { return defaults.blockCaret; } // /** // * Sets if the caret should be drawn as a block, false otherwise. // * @param blockCaret True if the caret should be drawn as a block, // * false otherwise. // */ // public final void setBlockCaretEnabled(boolean blockCaret) { // this.blockCaret = blockCaret; // invalidateSelectedLines(); // } // /** // * Returns the EOL marker color. // */ // public final Color getEOLMarkerColor() { // return eolMarkerColor; // } // /** // * Sets the EOL marker color. // * @param eolMarkerColor The EOL marker color // */ // public final void setEOLMarkerColor(Color eolMarkerColor) { // this.eolMarkerColor = eolMarkerColor; // repaint(); // } // /** // * Returns true if EOL markers are drawn, false otherwise. // */ // public final boolean getEOLMarkersPainted() { // return eolMarkers; // } // /** // * Sets if EOL markers are to be drawn. // * @param eolMarkers True if EOL markers should be drawn, false otherwise // */ // public final void setEOLMarkersPainted(boolean eolMarkers) { // this.eolMarkers = eolMarkers; // repaint(); // } // public final void setAntialias(boolean antialias) { // this.antialias = antialias; // } // /** // * Adds a custom highlight painter. // * @param highlight The highlight // */ // public void addCustomHighlight(Highlight highlight) { // highlight.init(textArea,highlights); // highlights = highlight; // } /** * Highlight interface. */ public interface Highlight { /** * Called after the highlight painter has been added. * @param textArea The text area * @param next The painter this one should delegate to */ void init(JEditTextArea textArea, Highlight next); /** * This should paint the highlight and delgate to the * next highlight painter. * @param gfx The graphics context * @param line The line number * @param y The y co-ordinate of the line */ void paintHighlight(Graphics gfx, int line, int y); /** * Returns the tool tip to display at the specified * location. If this highlighter doesn't know what to * display, it should delegate to the next highlight * painter. * @param evt The mouse event */ String getToolTipText(MouseEvent evt); } // /** // * Returns the tool tip to display at the specified location. // * @param evt The mouse event // */ // public String getToolTipText(MouseEvent evt) { // return (highlights == null) ? null : highlights.getToolTipText(evt); // } /** Returns the font metrics used by this component. */ public FontMetrics getFontMetrics() { return fm; } public FontMetrics getFontMetrics(SyntaxStyle style) { // return getFontMetrics(style.isBold() ? // defaults.boldFont : defaults.plainFont); return getFontMetrics(style.isBold() ? boldFont : plainFont); } // fry [160806 for 3.2] public int getLineHeight() { return fm.getHeight() + fm.getDescent(); } // /** // * Sets the font for this component. This is overridden to update the // * cached font metrics and to recalculate which lines are visible. // * @param font The font // */ // public void setFont(Font font) { //// new Exception().printStackTrace(System.out); // super.setFont(font); // fm = super.getFontMetrics(font); // textArea.recalculateVisibleLines(); // } /** * Repaints the text. * @param gfx The graphics context */ public void paint(Graphics gfx) { Graphics2D g2 = (Graphics2D) gfx; g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, antialias ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); // no effect, one way or the other // g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, // RenderingHints.VALUE_FRACTIONALMETRICS_ON); Rectangle clipRect = gfx.getClipBounds(); gfx.setColor(getBackground()); gfx.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height); // We don't use yToLine() here because that method doesn't // return lines past the end of the document int height = fm.getHeight(); int firstLine = textArea.getFirstLine(); int firstInvalid = firstLine + clipRect.y / height; // Because the clipRect's height is usually an even multiple // of the font height, we subtract 1 from it, otherwise one // too many lines will always be painted. int lastInvalid = firstLine + (clipRect.y + clipRect.height - 1) / height; try { TokenMarkerState tokenMarker = textArea.getDocument().getTokenMarker(); int x = textArea.getHorizontalOffset(); for (int line = firstInvalid; line <= lastInvalid; line++) { paintLine(gfx, line, x, tokenMarker); } if (tokenMarker != null && tokenMarker.isNextLineRequested()) { int h = clipRect.y + clipRect.height; repaint(0, h, getWidth(), getHeight() - h); } } catch (Exception e) { System.err.println("Error repainting line" + " range {" + firstInvalid + "," + lastInvalid + "}:"); e.printStackTrace(); } } /** * Marks a line as needing a repaint. * @param line The line to invalidate */ final public void invalidateLine(int line) { repaint(0, textArea.lineToY(line) + fm.getMaxDescent() + fm.getLeading(), getWidth(), fm.getHeight()); } /** * Marks a range of lines as needing a repaint. * @param firstLine The first line to invalidate * @param lastLine The last line to invalidate */ final void invalidateLineRange(int firstLine, int lastLine) { repaint(0,textArea.lineToY(firstLine) + fm.getMaxDescent() + fm.getLeading(), getWidth(),(lastLine - firstLine + 1) * fm.getHeight()); } /** Repaints the lines containing the selection. */ final void invalidateSelectedLines() { invalidateLineRange(textArea.getSelectionStartLine(), textArea.getSelectionStopLine()); } /** Returns next tab stop after a specified point. */ // TabExpander tabExpander = new TabExpander() { @Override public float nextTabStop(float x, int tabOffset) { int offset = textArea.getHorizontalOffset(); int ntabs = ((int)x - offset) / tabSize; return (ntabs + 1) * tabSize + offset; } // }; // do we go here? do will kill tabs? // public float nextTabStop(float x, int tabOffset) { // return x; // } public Dimension getPreferredSize() { return new Dimension(fm.charWidth('w') * defaults.cols, fm.getHeight() * defaults.rows); } public Dimension getMinimumSize() { return getPreferredSize(); } /** * Accessor used by tools that want to hook in and grab the formatting. */ public int getCurrentLineIndex() { return currentLineIndex; } /** * Accessor used by tools that want to hook in and grab the formatting. */ public void setCurrentLineIndex(int what) { currentLineIndex = what; } /** * Accessor used by tools that want to hook in and grab the formatting. */ public Token getCurrentLineTokens() { return currentLineTokens; } /** * Accessor used by tools that want to hook in and grab the formatting. */ public void setCurrentLineTokens(Token tokens) { currentLineTokens = tokens; } /** * Accessor used by tools that want to hook in and grab the formatting. */ public Segment getCurrentLine() { return currentLine; } // /** Old paintLine() method with kooky args order, kept around for X Mode. */ // @Deprecated // protected void paintLine(Graphics gfx, TokenMarker tokenMarker, // int line, int x) { // Font defaultFont = getFont(); // Color defaultColor = getForeground(); protected void paintLine(Graphics gfx, int line, int x, TokenMarkerState tokenMarker) { currentLineIndex = line; int y = textArea.lineToY(line); if (tokenMarker == null) { //paintPlainLine(gfx, line, defaultFont, defaultColor, x, y); paintPlainLine(gfx, line, x, y); } else if (line >= 0 && line < textArea.getLineCount()) { //paintSyntaxLine(gfx, tokenMarker, line, defaultFont, defaultColor, x, y); paintSyntaxLine(gfx, line, x, y, tokenMarker); } } // protected void paintLine(Graphics gfx, int line, int x, // TokenMarker tokenMarker) { // paintLine(gfx, tokenMarker, line, x); // } // protected void paintPlainLine(Graphics gfx, int line, Font defaultFont, // Color defaultColor, int x, int y) { protected void paintPlainLine(Graphics gfx, int line, int x, int y) { paintHighlight(gfx,line,y); textArea.getLineText(line, currentLine); // gfx.setFont(plainFont); // gfx.setFont(defaultFont); // gfx.setColor(defaultColor); int x0 = x - textArea.getHorizontalOffset(); y += fm.getHeight(); // doesn't respect fixed width like it should // x = Utilities.drawTabbedText(currentLine, x, y, gfx, this, 0); // int w = fm.charWidth(' '); for (int i = 0; i < currentLine.count; i++) { gfx.drawChars(currentLine.array, currentLine.offset+i, 1, x, y); x = currentLine.array[currentLine.offset + i] == '\t' ? x0 + (int)nextTabStop(x - x0, i) : x + fm.charWidth(currentLine.array[currentLine.offset+i]); textArea.offsetToX(line, currentLine.offset + i); } // Draw characters via input method. if (compositionTextPainter != null && compositionTextPainter.hasComposedTextLayout()) { compositionTextPainter.draw(gfx, defaults.lineHighlightColor); } if (defaults.eolMarkers) { gfx.setColor(defaults.eolMarkerColor); gfx.drawString(".", x, y); } } // protected void paintSyntaxLine(Graphics gfx, TokenMarker tokenMarker, // int line, Font defaultFont, // Color defaultColor, int x, int y) { protected void paintSyntaxLine(Graphics gfx, int line, int x, int y, TokenMarkerState tokenMarker) { textArea.getLineText(currentLineIndex, currentLine); currentLineTokens = tokenMarker.markTokens(currentLine, currentLineIndex); // gfx.setFont(plainFont); paintHighlight(gfx, line, y); // gfx.setFont(defaultFont); // gfx.setColor(defaultColor); y += fm.getHeight(); // x = paintSyntaxLine(currentLine, // currentLineTokens, // defaults.styles, this, gfx, x, y); x = paintSyntaxLine(gfx, currentLine, x, y, currentLineTokens, defaults.styles); // Draw characters via input method. if (compositionTextPainter != null && compositionTextPainter.hasComposedTextLayout()) { compositionTextPainter.draw(gfx, defaults.lineHighlightColor); } if (defaults.eolMarkers) { gfx.setColor(defaults.eolMarkerColor); gfx.drawString(".", x, y); } } /** * Paints the specified line onto the graphics context. Note that this * method munges the offset and count values of the segment. * @param line The line segment * @param tokens The token list for the line * @param styles The syntax style list * @param expander The tab expander used to determine tab stops. May * be null * @param gfx The graphics context * @param x The x co-ordinate * @param y The y co-ordinate * @return The x co-ordinate, plus the width of the painted string */ // public int paintSyntaxLine(Segment line, Token tokens, SyntaxStyle[] styles, // TabExpander expander, Graphics gfx, // int x, int y) { protected int paintSyntaxLine(Graphics gfx, Segment line, int x, int y, Token tokens, SyntaxStyle[] styles) { // Font defaultFont = gfx.getFont(); // Color defaultColor = gfx.getColor(); int x0 = x - textArea.getHorizontalOffset(); // for (byte id = tokens.id; id != Token.END; tokens = tokens.next) { for (;;) { byte id = tokens.id; if (id == Token.END) break; int length = tokens.length; if (id == Token.NULL) { // if(!defaultColor.equals(gfx.getColor())) // gfx.setColor(defaultColor); // if(!defaultFont.equals(gfx.getFont())) // gfx.setFont(defaultFont); gfx.setColor(defaults.fgcolor); gfx.setFont(plainFont); } else { //styles[id].setGraphicsFlags(gfx,defaultFont); SyntaxStyle ss = styles[id]; gfx.setColor(ss.getColor()); gfx.setFont(ss.isBold() ? boldFont : plainFont); } line.count = length; // huh? suspicious // doesn't respect mono metrics, insists on spacing w/ fractional or something // x = Utilities.drawTabbedText(line, x, y, gfx, this, 0); // gfx.drawChars(line.array, line.offset, line.count, x, y); // int w = fm.charWidth(' '); for (int i = 0; i < line.count; i++) { gfx.drawChars(line.array, line.offset+i, 1, x, y); x = line.array[line.offset + i] == '\t' ? x0 + (int)nextTabStop(x - x0, i) : x + fm.charWidth(line.array[line.offset+i]); } //x += fm.charsWidth(line.array, line.offset, line.count); //x += fm.charWidth(' ') * line.count; line.offset += length; tokens = tokens.next; } return x; } protected void paintHighlight(Graphics gfx, int line, int y) { if (line >= textArea.getSelectionStartLine() && line <= textArea.getSelectionStopLine()) { paintLineHighlight(gfx, line, y); } if (highlights != null) { highlights.paintHighlight(gfx, line, y); } if (defaults.bracketHighlight && line == textArea.getBracketLine()) { paintBracketHighlight(gfx, line, y); } if (line == textArea.getCaretLine()) { paintCaret(gfx, line, y); } } protected void paintLineHighlight(Graphics gfx, int line, int y) { int height = fm.getHeight(); y += fm.getLeading() + fm.getMaxDescent(); int selectionStart = textArea.getSelectionStart(); int selectionEnd = textArea.getSelectionStop(); if (selectionStart == selectionEnd) { if (defaults.lineHighlight) { gfx.setColor(defaults.lineHighlightColor); gfx.fillRect(0, y, getWidth(), height); } } else { gfx.setColor(defaults.selectionColor); int selectionStartLine = textArea.getSelectionStartLine(); int selectionEndLine = textArea.getSelectionStopLine(); int lineStart = textArea.getLineStartOffset(line); int x1, x2; if (selectionStartLine == selectionEndLine) { x1 = textArea._offsetToX(line, selectionStart - lineStart); x2 = textArea._offsetToX(line, selectionEnd - lineStart); } else if(line == selectionStartLine) { x1 = textArea._offsetToX(line, selectionStart - lineStart); x2 = getWidth(); } else if(line == selectionEndLine) { //x1 = 0; // hack from stendahl to avoid doing weird side selection thing x1 = textArea._offsetToX(line, 0); // attempt at getting the gutter too, but doesn't seem to work //x1 = textArea._offsetToX(line, -textArea.getHorizontalOffset()); x2 = textArea._offsetToX(line, selectionEnd - lineStart); } else { //x1 = 0; // hack from stendahl to avoid doing weird side selection thing x1 = textArea._offsetToX(line, 0); // attempt at getting the gutter too, but doesn't seem to work //x1 = textArea._offsetToX(line, -textArea.getHorizontalOffset()); x2 = getWidth(); } // "inlined" min/max() gfx.fillRect(x1 > x2 ? x2 : x1,y,x1 > x2 ? (x1 - x2) : (x2 - x1),height); } } protected void paintBracketHighlight(Graphics gfx, int line, int y) { int position = textArea.getBracketPosition(); if (position != -1) { y += fm.getLeading() + fm.getMaxDescent(); int x = textArea._offsetToX(line, position); gfx.setColor(defaults.bracketHighlightColor); // Hack!!! Since there is no fast way to get the character // from the bracket matching routine, we use ( since all // brackets probably have the same width anyway gfx.drawRect(x,y,fm.charWidth('(') - 1, fm.getHeight() - 1); } } protected void paintCaret(Graphics gfx, int line, int y) { //System.out.println("painting caret " + line + " " + y); if (textArea.isCaretVisible()) { //System.out.println("caret is visible"); int offset = textArea.getCaretPosition() - textArea.getLineStartOffset(line); int caretX = textArea._offsetToX(line, offset); int caretWidth = ((defaults.blockCaret || textArea.isOverwriteEnabled()) ? fm.charWidth('w') : 1); y += fm.getLeading() + fm.getMaxDescent(); int height = fm.getHeight(); //System.out.println("caretX, width = " + caretX + " " + caretWidth); gfx.setColor(defaults.caretColor); if (textArea.isOverwriteEnabled()) { gfx.fillRect(caretX, y + height - 1, caretWidth,1); } else { // some machines don't like the drawRect for the single // pixel caret.. this caused a lot of hell because on that // minority of machines, the caret wouldn't show up past // the first column. the fix is to use drawLine() in // those cases, as a workaround. if (caretWidth == 1) { gfx.drawLine(caretX, y, caretX, y + height - 1); } else { gfx.drawRect(caretX, y, caretWidth - 1, height - 1); } //gfx.drawRect(caretX, y, caretWidth, height - 1); } } } public int getScrollWidth() { // https://github.com/processing/processing/issues/3591 return super.getWidth(); } }