/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2012-16 The Processing Foundation 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. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package processing.app.syntax; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.GeneralPath; import java.util.List; import javax.swing.text.BadLocationException; import javax.swing.text.Segment; import javax.swing.text.Utilities; import processing.app.Mode; import processing.app.Problem; import processing.app.ui.Editor; /** * Adds support to TextAreaPainter for background colors, * and the left hand gutter area with background color and text. */ public class PdeTextAreaPainter extends TextAreaPainter { public Color errorUnderlineColor; public Color warningUnderlineColor; protected Font gutterTextFont; protected Color gutterTextColor; protected Color gutterPastColor; protected Color gutterLineHighlightColor; public PdeTextAreaPainter(JEditTextArea textArea, TextAreaDefaults defaults) { super(textArea, defaults); // Handle mouse clicks to toggle breakpoints addMouseListener(new MouseAdapter() { long lastTime; // OS X seems to be firing multiple mouse events public void mousePressed(MouseEvent event) { // Don't toggle breakpoints when the debugger isn't enabled // https://github.com/processing/processing/issues/3306 if (getEditor().isDebuggerEnabled()) { long thisTime = event.getWhen(); if (thisTime - lastTime > 100) { if (event.getX() < Editor.LEFT_GUTTER) { int offset = textArea.xyToOffset(event.getX(), event.getY()); if (offset >= 0) { int lineIndex = textArea.getLineOfOffset(offset); getEditor().toggleBreakpoint(lineIndex); } } lastTime = thisTime; } } } }); } /** * Loads theme for TextAreaPainter. This is handled here because in the olden * days, Modes had different visual design. Now, these are just pulling the * defaults from the standard theme, though there may be minor additions or * overrides added in a Mode's own theme.txt file. */ public void setMode(Mode mode) { errorUnderlineColor = mode.getColor("editor.error.underline.color"); warningUnderlineColor = mode.getColor("editor.warning.underline.color"); gutterTextFont = mode.getFont("editor.gutter.text.font"); gutterTextColor = mode.getColor("editor.gutter.text.color"); gutterPastColor = new Color(gutterTextColor.getRed(), gutterTextColor.getGreen(), gutterTextColor.getBlue(), 96); gutterLineHighlightColor = mode.getColor("editor.gutter.linehighlight.color"); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . /** * Paint a line. Paints the gutter (with background color and text) * then the line (background color and text). * * @param gfx the graphics context * @param tokenMarker * @param line 0-based line number * @param x horizontal position */ @Override protected void paintLine(Graphics gfx, int line, int x, TokenMarkerState marker) { try { // TODO This line is causing NPEs randomly ever since I added the // toggle for Java Mode/Debugger toolbar. [Manindra] super.paintLine(gfx, line, x + Editor.LEFT_GUTTER, marker); } catch (Exception e) { e.printStackTrace(); } paintLeftGutter(gfx, line, x); paintErrorLine(gfx, line, x); } /** * Paints the underline for an error/warning line */ protected void paintErrorLine(Graphics gfx, int line, int x) { List<Problem> problems = getEditor().findProblems(line); for (Problem problem : problems) { int startOffset = problem.getStartOffset(); int stopOffset = problem.getStopOffset(); int lineOffset = textArea.getLineStartOffset(line); int wiggleStart = Math.max(startOffset, lineOffset); int wiggleStop = Math.min(stopOffset, textArea.getLineStopOffset(line)); int y = textArea.lineToY(line) + fm.getLeading() + fm.getMaxDescent(); try { String badCode = null; String goodCode = null; try { SyntaxDocument doc = textArea.getDocument(); badCode = doc.getText(wiggleStart, wiggleStop - wiggleStart); goodCode = doc.getText(lineOffset, wiggleStart - lineOffset); //log("paintErrorLine() LineText GC: " + goodCode); //log("paintErrorLine() LineText BC: " + badCode); } catch (BadLocationException bl) { // Error in the import statements or end of code. // System.out.print("BL caught. " + ta.getLineCount() + " ," // + line + " ,"); // log((ta.getLineStopOffset(line) - start - 1)); return; } int trimmedLength = badCode.trim().length(); int rightTrimmedLength = trimRight(badCode).length(); int leftTrimLength = rightTrimmedLength - trimmedLength; // Fix offsets when bad code is just whitespace if (trimmedLength == 0) { leftTrimLength = 0; rightTrimmedLength = badCode.length(); } int x1 = textArea.offsetToX(line, goodCode.length() + leftTrimLength); int x2 = textArea.offsetToX(line, goodCode.length() + rightTrimmedLength); if (x1 == x2) x2 += fm.stringWidth(" "); int y1 = y + fm.getHeight() - 2; if (line != problem.getLineNumber()) { x1 = Editor.LEFT_GUTTER; // on the following lines, wiggle extends to the left border } gfx.setColor(errorUnderlineColor); if (problem.isWarning()) { gfx.setColor(warningUnderlineColor); } paintSquiggle(gfx, y1, x1, x2); } catch (Exception e) { e.printStackTrace(); } } } /** * Paint the gutter: draw the background, draw line numbers, break points. * @param gfx the graphics context * @param line 0-based line number * @param x horizontal position */ protected void paintLeftGutter(Graphics gfx, int line, int x) { int y = textArea.lineToY(line) + fm.getLeading() + fm.getMaxDescent(); if (line == textArea.getSelectionStopLine()) { gfx.setColor(gutterLineHighlightColor); gfx.fillRect(0, y, Editor.LEFT_GUTTER, fm.getHeight()); } else { //gfx.setColor(getJavaTextArea().gutterBgColor); gfx.setClip(0, y, Editor.LEFT_GUTTER, fm.getHeight()); gfx.drawImage(((PdeTextArea) textArea).getGutterGradient(), 0, 0, getWidth(), getHeight(), this); gfx.setClip(null); // reset } String text = null; if (getEditor().isDebuggerEnabled()) { text = getPdeTextArea().getGutterText(line); } gfx.setColor(line < textArea.getLineCount() ? gutterTextColor : gutterPastColor); // if (line >= textArea.getLineCount()) { // //gfx.setColor(new Color(gutterTextColor.getRGB(), ); // } int textRight = Editor.LEFT_GUTTER - Editor.GUTTER_MARGIN; int textBaseline = textArea.lineToY(line) + fm.getHeight(); if (text != null) { if (text.equals(PdeTextArea.BREAK_MARKER)) { drawDiamond(gfx, textRight - 8, textBaseline - 8, 8, 8); } else if (text.equals(PdeTextArea.STEP_MARKER)) { //drawRightArrow(gfx, textRight - 7, textBaseline - 7, 7, 6); drawRightArrow(gfx, textRight - 7, textBaseline - 7.5f, 7, 7); } } else { // if no special text for a breakpoint, just show the line number text = String.valueOf(line + 1); //text = makeOSF(String.valueOf(line + 1)); gfx.setFont(gutterTextFont); // ((Graphics2D) gfx).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, // RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); // Right-align the text char[] txt = text.toCharArray(); int tx = textRight - gfx.getFontMetrics().charsWidth(txt, 0, txt.length); // Using 'fm' here because it's relative to the editor text size, // not the numbers in the gutter Utilities.drawTabbedText(new Segment(txt, 0, text.length()), tx, textBaseline, gfx, this, 0); } } static private void drawDiamond(Graphics g, float x, float y, float w, float h) { Graphics2D g2 = (Graphics2D) g; GeneralPath path = new GeneralPath(); path.moveTo(x + w/2, y); path.lineTo(x + w, y + h/2); path.lineTo(x + w/2, y + h); path.lineTo(x, y + h/2); path.closePath(); g2.fill(path); } static private void drawRightArrow(Graphics g, float x, float y, float w, float h) { Graphics2D g2 = (Graphics2D) g; GeneralPath path = new GeneralPath(); path.moveTo(x, y); path.lineTo(x + w, y + h/2); path.lineTo(x, y + h); path.closePath(); g2.fill(path); } /** * Remove all trailing whitespace from a line */ static private String trimRight(String str) { int i = str.length() - 1; while (i >= 0 && Character.isWhitespace(str.charAt(i))) { i--; } return str.substring(0, i+1); } static private void paintSquiggle(Graphics g, int y, int x1, int x2) { int xx = x1; while (xx < x2) { g.drawLine(xx, y, xx + 2, y + 1); xx += 2; g.drawLine(xx, y + 1, xx + 2, y); xx += 2; } } @Override public String getToolTipText(MouseEvent event) { int line = event.getY() / getFontMetrics().getHeight() + textArea.getFirstLine(); if (line >= 0 || line < textArea.getLineCount()) { List<Problem> problems = getEditor().findProblems(line); for (Problem problem : problems) { int lineStart = textArea.getLineStartOffset(line); int lineEnd = textArea.getLineStopOffset(line); int errorStart = problem.getStartOffset(); int errorEnd = problem.getStopOffset() + 1; int startOffset = Math.max(errorStart, lineStart) - lineStart; int stopOffset = Math.min(errorEnd, lineEnd) - lineStart; int x = event.getX(); if (x >= textArea.offsetToX(line, startOffset) && x <= textArea.offsetToX(line, stopOffset)) { getEditor().statusToolTip(this, problem.getMessage(), problem.isError()); return super.getToolTipText(event); } } } setToolTipText(null); return super.getToolTipText(event); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . @Override public int getScrollWidth() { // TODO https://github.com/processing/processing/issues/3591 return super.getWidth() - Editor.LEFT_GUTTER; } public Editor getEditor() { return getPdeTextArea().editor; } public PdeTextArea getPdeTextArea() { return (PdeTextArea) textArea; } }