/* * Copyright (C) 2012-14 Martin Leopold and Manindra Moharana * * 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 2 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, write to the Free Software Foundation, Inc., 59 Temple * Place - Suite 330, Boston, MA 02111-1307, USA. */ package processing.mode.experimental; import static processing.mode.experimental.ExperimentalMode.log; import static processing.mode.experimental.ExperimentalMode.log2; import galsasson.mode.tweak.ColorControlBox; import galsasson.mode.tweak.Handle; import java.awt.Color; import java.awt.Cursor; import java.awt.FontMetrics; import java.awt.Point; import java.awt.event.ComponentListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.DefaultListModel; import javax.swing.SwingWorker; import processing.app.Base; import processing.app.syntax.JEditTextArea; import processing.app.syntax.TextAreaDefaults; /** * Customized text area. Adds support for line background colors. * * @author Martin Leopold */ public class TextArea extends JEditTextArea { protected MouseListener[] mouseListeners; // cached mouselisteners, these are wrapped by MouseHandler protected DebugEditor editor; // the editor // line properties protected Map lineColors = new HashMap(); // contains line background colors // left-hand gutter properties protected int gutterPadding = 3; // [px] space added to the left and right of gutter chars protected Color gutterBgColor = new Color(252, 252, 252); // gutter background color protected Color gutterLineColor = new Color(233, 233, 233); // color of vertical separation line protected String breakpointMarker = "<>"; // the text marker for highlighting breakpoints in the gutter protected String currentLineMarker = "->"; // the text marker for highlighting the current line in the gutter protected Map gutterText = new HashMap(); // maps line index to gutter text protected Map gutterTextColors = new HashMap(); // maps line index to gutter text color protected TextAreaPainter customPainter; protected ErrorCheckerService errorCheckerService; public TextArea(TextAreaDefaults defaults, DebugEditor editor) { super(defaults); this.editor = editor; // replace the painter: // first save listeners, these are package-private in JEditTextArea, so not accessible ComponentListener[] componentListeners = painter.getComponentListeners(); mouseListeners = painter.getMouseListeners(); MouseMotionListener[] mouseMotionListeners = painter .getMouseMotionListeners(); remove(painter); // set new painter customPainter = new TextAreaPainter(this, defaults); painter = customPainter; // set listeners for (ComponentListener cl : componentListeners) { painter.addComponentListener(cl); } for (MouseMotionListener mml : mouseMotionListeners) { painter.addMouseMotionListener(mml); } // use a custom mouse handler instead of directly using mouseListeners MouseHandler mouseHandler = new MouseHandler(); painter.addMouseListener(mouseHandler); painter.addMouseMotionListener(mouseHandler); //addCompletionPopupListner(); add(CENTER, painter); // load settings from theme.txt ExperimentalMode theme = (ExperimentalMode) editor.getMode(); gutterBgColor = theme.getThemeColor("gutter.bgcolor", gutterBgColor); gutterLineColor = theme.getThemeColor("gutter.linecolor", gutterLineColor); gutterPadding = theme.getInteger("gutter.padding"); breakpointMarker = theme.loadThemeString("breakpoint.marker", breakpointMarker); currentLineMarker = theme.loadThemeString("currentline.marker", currentLineMarker); // TweakMode code prevCompListeners = painter .getComponentListeners(); prevMouseListeners = painter.getMouseListeners(); prevMMotionListeners = painter .getMouseMotionListeners(); prevKeyListeners = editor.getKeyListeners(); interactiveMode = false; addPrevListeners(); } /** * Sets ErrorCheckerService and loads theme for TextArea(XQMode) * * @param ecs * @param mode */ public void setECSandThemeforTextArea(ErrorCheckerService ecs, ExperimentalMode mode) { errorCheckerService = ecs; customPainter.setECSandTheme(ecs, mode); } /** * Handles KeyEvents for TextArea * Code completion begins from here. */ public void processKeyEvent(KeyEvent evt) { //if(Base.isMacOS() && evt.isControlDown()) System.out.println("Ctrl down: " + evt); if(evt.getKeyCode() == KeyEvent.VK_ESCAPE){ if(suggestion != null){ if(suggestion.isVisible()){ log("esc key"); hideSuggestion(); evt.consume(); return; } } } else if(evt.getKeyCode() == KeyEvent.VK_ENTER && evt.getID() == KeyEvent.KEY_PRESSED){ if (suggestion != null) { if (suggestion.isVisible()) { if (suggestion.insertSelection(CompletionPanel.KEYBOARD_COMPLETION)) { //hideSuggestion(); // Kill it! evt.consume(); // Still try to show suggestions after inserting if it's // the case of overloaded methods. See #2755 if(suggestion.isVisible()) prepareSuggestions(evt); return; } } } } if (evt.getID() == KeyEvent.KEY_PRESSED) { switch (evt.getKeyCode()) { case KeyEvent.VK_DOWN: if (suggestion != null) if (suggestion.isVisible()) { //log("KeyDown"); suggestion.moveDown(); return; } break; case KeyEvent.VK_UP: if (suggestion != null) if (suggestion.isVisible()) { //log("KeyUp"); suggestion.moveUp(); return; } break; case KeyEvent.VK_BACK_SPACE: log("BK Key"); break; case KeyEvent.VK_SPACE: if (suggestion != null) if (suggestion.isVisible()) { log("Space bar, hide completion list"); suggestion.hide(); } break; default: break; } } super.processKeyEvent(evt); if (editor.hasJavaTabs) return; // code completion disabled if java tabs if (evt.getID() == KeyEvent.KEY_TYPED) { char keyChar = evt.getKeyChar(); if (keyChar == KeyEvent.VK_ENTER || keyChar == KeyEvent.VK_ESCAPE || keyChar == KeyEvent.VK_TAB || keyChar == KeyEvent.CHAR_UNDEFINED) { return; } else if (keyChar == ')') { hideSuggestion(); // See #2741 return; } final KeyEvent evt2 = evt; if (keyChar == '.') { if (ExperimentalMode.codeCompletionsEnabled) { log("[KeyEvent]" + KeyEvent.getKeyText(evt2.getKeyCode()) + " |Prediction started"); log("Typing: " + fetchPhrase(evt2)); } } else if (keyChar == ' ') { // Trigger on Ctrl-Space if (!Base.isMacOS() && ExperimentalMode.codeCompletionsEnabled && (evt.isControlDown() || evt.isMetaDown())) { SwingWorker worker = new SwingWorker() { protected Object doInBackground() throws Exception { // Provide completions only if it's enabled if (ExperimentalMode.codeCompletionsEnabled) { getDocument().remove(getCaretPosition() - 1, 1); // Remove the typed space log("[KeyEvent]" + evt2.getKeyChar() + " |Prediction started"); log("Typing: " + fetchPhrase(evt2)); } return null; } }; worker.execute(); } else { hideSuggestion(); // hide on spacebar } } else { if(ExperimentalMode.codeCompletionsEnabled) { prepareSuggestions(evt2); } } } // #2699 - Special case for OS X, where Ctrl-Space is not detected as Key_Typed -_- else if (Base.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED && evt.getKeyCode() == KeyEvent.VK_SPACE && evt.isControlDown()) { final KeyEvent evt2 = evt; SwingWorker worker = new SwingWorker() { protected Object doInBackground() throws Exception { // Provide completions only if it's enabled if (ExperimentalMode.codeCompletionsEnabled) { log("[KeyEvent]" + KeyEvent.getKeyText(evt2.getKeyCode()) + " |Prediction started"); log("Typing: " + fetchPhrase(evt2)); } return null; } }; worker.execute(); } } /** * Kickstart auto-complete suggestions * @param evt - KeyEvent */ private void prepareSuggestions(final KeyEvent evt){ SwingWorker worker = new SwingWorker() { protected Object doInBackground() throws Exception { // Provide completions only if it's enabled if (ExperimentalMode.codeCompletionsEnabled && (ExperimentalMode.ccTriggerEnabled || suggestion.isVisible())) { log("[KeyEvent]" + evt.getKeyChar() + " |Prediction started"); log("Typing: " + fetchPhrase(evt)); } return null; } }; worker.execute(); } /** * Retrieves the word on which the mouse pointer is present * @param evt - the MouseEvent which triggered this method * @return */ private String fetchPhrase(MouseEvent evt) { log("--handle Mouse Right Click--"); int off = xyToOffset(evt.getX(), evt.getY()); if (off < 0) return null; int line = getLineOfOffset(off); if (line < 0) return null; String s = getLineText(line); if (s == null) return null; else if (s.length() == 0) return null; else { int x = xToOffset(line, evt.getX()), x2 = x + 1, x1 = x - 1; int xLS = off - getLineStartNonWhiteSpaceOffset(line); log("x=" + x); if (x < 0 || x >= s.length()) return null; String word = s.charAt(x) + ""; if (s.charAt(x) == ' ') return null; if (!(Character.isLetterOrDigit(s.charAt(x)) || s.charAt(x) == '_' || s .charAt(x) == '$')) return null; int i = 0; while (true) { i++; if (x1 >= 0 && x1 < s.length()) { if (Character.isLetter(s.charAt(x1)) || s.charAt(x1) == '_') { word = s.charAt(x1--) + word; xLS--; } else x1 = -1; } else x1 = -1; if (x2 >= 0 && x2 < s.length()) { if (Character.isLetterOrDigit(s.charAt(x2)) || s.charAt(x2) == '_' || s.charAt(x2) == '$') word = word + s.charAt(x2++); else x2 = -1; } else x2 = -1; if (x1 < 0 && x2 < 0) break; if (i > 200) { // time out! break; } } if (Character.isDigit(word.charAt(0))) return null; log("Mouse click, word: " + word.trim()); errorCheckerService.getASTGenerator().setLastClickedWord(line, word, xLS); return word.trim(); } } /** * Retrieves the current word typed just before the caret. * Then triggers code completion for that word. * * @param evt - the KeyEvent which triggered this method * @return */ public String fetchPhrase(KeyEvent evt) { int off = getCaretPosition(); log2("off " + off); if (off < 0) return null; int line = getCaretLine(); if (line < 0) return null; String s = getLineText(line); log2("lin " + line); //log2(s + " len " + s.length()); int x = getCaretPosition() - getLineStartOffset(line) - 1, x1 = x - 1; if(x >= s.length() || x < 0) { //log("X is " + x + ". Returning null"); hideSuggestion(); return null; //TODO: Does this check cause problems? Verify. } log2(" x char: " + s.charAt(x)); if (!(Character.isLetterOrDigit(s.charAt(x)) || s.charAt(x) == '_' || s.charAt(x) == '(' || s.charAt(x) == '.')) { //log("Char before caret isn't a letter/digit/_(. so no predictions"); hideSuggestion(); return null; } else if (x > 0 && (s.charAt(x - 1) == ' ' || s.charAt(x - 1) == '(') && Character.isDigit(s.charAt(x))) { //log("Char before caret isn't a letter, but ' ' or '(', so no predictions"); hideSuggestion(); // See #2755, Option 2 comment return null; } else if (x == 0){ //log("X is zero"); hideSuggestion(); return null; } //int xLS = off - getLineStartNonWhiteSpaceOffset(line); String word = (x < s.length() ? s.charAt(x) : "") + ""; if (s.trim().length() == 1) { // word = "" // + (keyChar == KeyEvent.CHAR_UNDEFINED ? s.charAt(x - 1) : keyChar); //word = (x < s.length()?s.charAt(x):"") + ""; word = word.trim(); if (word.endsWith(".")) word = word.substring(0, word.length() - 1); errorCheckerService.getASTGenerator().preparePredictions(word, line + errorCheckerService.mainClassOffset,0); return word; } int i = 0; int closeB = 0; while (true) { i++; //TODO: currently works on single line only. "a. b()" won't be detected if (x1 >= 0) { // if (s.charAt(x1) != ';' && s.charAt(x1) != ',' && s.charAt(x1) != '(') if (Character.isLetterOrDigit(s.charAt(x1)) || s.charAt(x1) == '_' || s.charAt(x1) == '.' || s.charAt(x1) == ')' || s.charAt(x1) == ']') { if (s.charAt(x1) == ')') { word = s.charAt(x1--) + word; closeB++; while (x1 >= 0 && closeB > 0) { word = s.charAt(x1) + word; if (s.charAt(x1) == '(') closeB--; if (s.charAt(x1) == ')') closeB++; x1--; } } else if (s.charAt(x1) == ']') { word = s.charAt(x1--) + word; closeB++; while (x1 >= 0 && closeB > 0) { word = s.charAt(x1) + word; if (s.charAt(x1) == '[') closeB--; if (s.charAt(x1) == ']') closeB++; x1--; } } else { word = s.charAt(x1--) + word; } } else { break; } } else { break; } if (i > 200) { // time out! break; } } if (Character.isDigit(word.charAt(0))) return null; word = word.trim(); // if (word.endsWith(".")) // word = word.substring(0, word.length() - 1); int lineStartNonWSOffset = 0; if (word.length() >= ExperimentalMode.codeCompletionTriggerLength) { errorCheckerService.getASTGenerator() .preparePredictions(word, line + errorCheckerService.mainClassOffset, lineStartNonWSOffset); } return word; } /** * Retrieve the total width of the gutter area. * * @return gutter width in pixels */ protected int getGutterWidth() { if(editor.debugToolbarEnabled == null || !editor.debugToolbarEnabled.get()){ return 0; } FontMetrics fm = painter.getFontMetrics(); // log("fm: " + (fm == null)); // log("editor: " + (editor == null)); //log("BPBPBPBPB: " + (editor.breakpointMarker == null)); int textWidth = Math.max(fm.stringWidth(breakpointMarker), fm.stringWidth(currentLineMarker)); return textWidth + 2 * gutterPadding; } /** * Retrieve the width of margins applied to the left and right of the gutter * text. * * @return margins in pixels */ protected int getGutterMargins() { if(editor.debugToolbarEnabled == null || !editor.debugToolbarEnabled.get()){ return 0; } return gutterPadding; } /** * Set the gutter text of a specific line. * * @param lineIdx * the line index (0-based) * @param text * the text */ public void setGutterText(int lineIdx, String text) { gutterText.put(lineIdx, text); painter.invalidateLine(lineIdx); } /** * Set the gutter text and color of a specific line. * * @param lineIdx * the line index (0-based) * @param text * the text * @param textColor * the text color */ public void setGutterText(int lineIdx, String text, Color textColor) { gutterTextColors.put(lineIdx, textColor); setGutterText(lineIdx, text); } /** * Clear the gutter text of a specific line. * * @param lineIdx * the line index (0-based) */ public void clearGutterText(int lineIdx) { gutterText.remove(lineIdx); painter.invalidateLine(lineIdx); } /** * Clear all gutter text. */ public void clearGutterText() { for (int lineIdx : gutterText.keySet()) { painter.invalidateLine(lineIdx); } gutterText.clear(); } /** * Retrieve the gutter text of a specific line. * * @param lineIdx * the line index (0-based) * @return the gutter text */ public String getGutterText(int lineIdx) { return gutterText.get(lineIdx); } /** * Retrieve the gutter text color for a specific line. * * @param lineIdx * the line index * @return the gutter text color */ public Color getGutterTextColor(int lineIdx) { return gutterTextColors.get(lineIdx); } /** * Set the background color of a line. * * @param lineIdx * 0-based line number * @param col * the background color to set */ public void setLineBgColor(int lineIdx, Color col) { lineColors.put(lineIdx, col); painter.invalidateLine(lineIdx); } /** * Clear the background color of a line. * * @param lineIdx * 0-based line number */ public void clearLineBgColor(int lineIdx) { lineColors.remove(lineIdx); painter.invalidateLine(lineIdx); } /** * Clear all line background colors. */ public void clearLineBgColors() { for (int lineIdx : lineColors.keySet()) { painter.invalidateLine(lineIdx); } lineColors.clear(); } /** * Get a lines background color. * * @param lineIdx * 0-based line number * @return the color or null if no color was set for the specified line */ public Color getLineBgColor(int lineIdx) { return lineColors.get(lineIdx); } /** * Convert a character offset to a horizontal pixel position inside the text * area. Overridden to take gutter width into account. * * @param line * the 0-based line number * @param offset * the character offset (0 is the first character on a line) * @return the horizontal position */ @Override public int _offsetToX(int line, int offset) { return super._offsetToX(line, offset) + getGutterWidth(); } /** * Convert a horizontal pixel position to a character offset. Overridden to * take gutter width into account. * * @param line * the 0-based line number * @param x * the horizontal pixel position * @return he character offset (0 is the first character on a line) */ @Override public int xToOffset(int line, int x) { return super.xToOffset(line, x - getGutterWidth()); } /** * Custom mouse handler. Implements double clicking in the gutter area to * toggle breakpoints, sets default cursor (instead of text cursor) in the * gutter area. */ protected class MouseHandler implements MouseListener, MouseMotionListener { protected int lastX; // previous horizontal positon of the mouse cursor @Override public void mouseClicked(MouseEvent me) { // forward to standard listeners for (MouseListener ml : mouseListeners) { ml.mouseClicked(me); } } @Override public void mousePressed(MouseEvent me) { // check if this happened in the gutter area if (me.getX() < getGutterWidth()) { if (me.getButton() == MouseEvent.BUTTON1 && me.getClickCount() == 2) { int line = me.getY() / painter.getFontMetrics().getHeight() + firstLine; if (line >= 0 && line <= getLineCount() - 1) { editor.gutterDblClicked(line); } } return; } if (me.getButton() == MouseEvent.BUTTON3) { if(!editor.hasJavaTabs){ // tooltips, etc disabled for java tabs fetchPhrase(me); } } // forward to standard listeners for (MouseListener ml : mouseListeners) { ml.mousePressed(me); } } @Override public void mouseReleased(MouseEvent me) { // forward to standard listeners for (MouseListener ml : mouseListeners) { ml.mouseReleased(me); } } @Override public void mouseEntered(MouseEvent me) { // forward to standard listeners for (MouseListener ml : mouseListeners) { ml.mouseEntered(me); } } @Override public void mouseExited(MouseEvent me) { // forward to standard listeners for (MouseListener ml : mouseListeners) { ml.mouseExited(me); } } @Override public void mouseDragged(MouseEvent me) { // No need to forward since the standard MouseMotionListeners are called anyway // nop } @Override public void mouseMoved(MouseEvent me) { // No need to forward since the standard MouseMotionListeners are called anyway if (me.getX() < getGutterWidth()) { if (lastX >= getGutterWidth()) { painter.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); } } else { if (lastX < getGutterWidth()) { painter.setCursor(new Cursor(Cursor.TEXT_CURSOR)); } } lastX = me.getX(); } } private CompletionPanel suggestion; //JEditTextArea textarea; /* No longer used private void addCompletionPopupListner() { this.addKeyListener(new KeyListener() { @Override public void keyTyped(KeyEvent e) { } @Override public void keyReleased(KeyEvent e) { if (Character.isLetterOrDigit(e.getKeyChar()) || e.getKeyChar() == KeyEvent.VK_BACK_SPACE || e.getKeyChar() == KeyEvent.VK_DELETE) { // SwingUtilities.invokeLater(new Runnable() { // @Override // public void run() { // showSuggestion(); // } // // }); } else if (Character.isWhitespace(e.getKeyChar()) || e.getKeyChar() == KeyEvent.VK_ESCAPE) { hideSuggestion(); } } @Override public void keyPressed(KeyEvent e) { } }); }*/ // appears unused, removed when looking to change completion trigger [fry 140801] /* public void showSuggestionLater(final DefaultListModel defListModel, final String word) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { showSuggestion(defListModel,word); } }); } */ /** * Calculates location of caret and displays the suggestion popup at the location. * * @param defListModel * @param subWord */ protected void showSuggestion(DefaultListModel defListModel,String subWord) { hideSuggestion(); if (defListModel.size() == 0) { log("TextArea: No suggestions to show."); return; } int position = getCaretPosition(); Point location = new Point(); try { location.x = offsetToX(getCaretLine(), position - getLineStartOffset(getCaretLine())); location.y = lineToY(getCaretLine()) + getPainter().getFontMetrics().getHeight() + getPainter().getFontMetrics().getDescent(); //log("TA position: " + location); } catch (Exception e2) { e2.printStackTrace(); return; } if (subWord.length() < 2) { return; } //if (suggestion == null) suggestion = new CompletionPanel(this, position, subWord, defListModel, location,editor); // else // suggestion.updateList(defListModel, subWord, location, position); // // suggestion.setVisible(true); requestFocusInWindow(); // SwingUtilities.invokeLater(new Runnable() { // @Override // public void run() { // requestFocusInWindow(); // } // }); } /** * Hides suggestion popup */ protected void hideSuggestion() { if (suggestion != null) { suggestion.hide(); //log("Suggestion hidden."); suggestion = null; } } // TweakMode code // save input listeners to stop/start text edit ComponentListener[] prevCompListeners; MouseListener[] prevMouseListeners; MouseMotionListener[] prevMMotionListeners; KeyListener[] prevKeyListeners; boolean interactiveMode; /* remove all standard interaction listeners */ public void removeAllListeners() { ComponentListener[] componentListeners = painter .getComponentListeners(); MouseListener[] mouseListeners = painter.getMouseListeners(); MouseMotionListener[] mouseMotionListeners = painter .getMouseMotionListeners(); KeyListener[] keyListeners = editor.getKeyListeners(); for (ComponentListener cl : componentListeners) painter.removeComponentListener(cl); for (MouseListener ml : mouseListeners) painter.removeMouseListener(ml); for (MouseMotionListener mml : mouseMotionListeners) painter.removeMouseMotionListener(mml); for (KeyListener kl : keyListeners) { editor.removeKeyListener(kl); } } public void startInteractiveMode() { // ignore if we are already in interactiveMode if (interactiveMode) return; removeAllListeners(); // add our private interaction listeners customPainter.addMouseListener(customPainter); customPainter.addMouseMotionListener(customPainter); customPainter.startInterativeMode(); customPainter.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); this.editable = false; this.caretBlinks = false; this.setCaretVisible(false); interactiveMode = true; } public void stopInteractiveMode() { // ignore if we are not in interactive mode if (!interactiveMode) return; removeAllListeners(); addPrevListeners(); customPainter.stopInteractiveMode(); customPainter.setCursor(new Cursor(Cursor.TEXT_CURSOR)); this.editable = true; this.caretBlinks = true; this.setCaretVisible(true); interactiveMode = false; } public int getHorizontalScroll() { return horizontal.getValue(); } private void addPrevListeners() { // add the original text-edit listeners for (ComponentListener cl : prevCompListeners) { customPainter.addComponentListener(cl); } for (MouseListener ml : prevMouseListeners) { customPainter.addMouseListener(ml); } for (MouseMotionListener mml : prevMMotionListeners) { customPainter.addMouseMotionListener(mml); } for (KeyListener kl : prevKeyListeners) { editor.addKeyListener(kl); } } //public void updateInterface(ArrayList handles[], ArrayList colorBoxes[]) { public void updateInterface(List> handles, List> colorBoxes) { customPainter.updateInterface(handles, colorBoxes); } }