/* -*- mode: jde; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2004-05 Ben Fry and Casey Reas Copyright (c) 2001-04 Massachusetts Institute of Technology 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.app; import processing.app.syntax.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.text.*; import javax.swing.event.*; /** * Filters key events for tab expansion/indent/etc. *
* For version 0099, some changes have been made to make the indents * smarter. There are still issues though: * + indent happens when it picks up a curly brace on the previous line, * but not if there's a blank line between them. * + It also doesn't handle single indent situations where a brace * isn't used (i.e. an if statement or for loop that's a single line). * It shouldn't actually be using braces. * Solving these issues, however, would probably best be done by a * smarter parser/formatter, rather than continuing to hack this class. */ public class EditorListener { Editor editor; JEditTextArea textarea; boolean externalEditor; boolean tabsExpand; boolean tabsIndent; int tabSize; String tabString; boolean autoIndent; int selectionStart, selectionEnd; int position; public EditorListener(Editor editor, JEditTextArea textarea) { this.editor = editor; this.textarea = textarea; // let him know that i'm leechin' textarea.editorListener = this; applyPreferences(); } public void applyPreferences() { tabsExpand = Preferences.getBoolean("editor.tabs.expand"); //tabsIndent = Preferences.getBoolean("editor.tabs.indent"); tabSize = Preferences.getInteger("editor.tabs.size"); tabString = Editor.EMPTY.substring(0, tabSize); autoIndent = Preferences.getBoolean("editor.indent"); externalEditor = Preferences.getBoolean("editor.external"); } //public void setExternalEditor(boolean externalEditor) { //this.externalEditor = externalEditor; //} /** * Intercepts key pressed events for JEditTextArea. * * Called by JEditTextArea inside processKeyEvent(). Note that this * won't intercept actual characters, because those are fired on * keyTyped(). */ public boolean keyPressed(KeyEvent event) { // don't do things if the textarea isn't editable if (externalEditor) return false; //deselect(); // this is for paren balancing char c = event.getKeyChar(); int code = event.getKeyCode(); //System.out.println(c + " " + code + " " + event); //System.out.println(); if ((event.getModifiers() & KeyEvent.META_MASK) != 0) { //event.consume(); // does nothing return false; } // TODO i don't like these accessors. clean em up later. if (!editor.sketch.modified) { if ((code == KeyEvent.VK_BACK_SPACE) || (code == KeyEvent.VK_TAB) || (code == KeyEvent.VK_ENTER) || ((c >= 32) && (c < 128))) { editor.sketch.setModified(true); } } if ((code == KeyEvent.VK_UP) && ((event.getModifiers() & KeyEvent.CTRL_MASK) != 0)) { // back up to the last empty line char contents[] = textarea.getText().toCharArray(); //int origIndex = textarea.getCaretPosition() - 1; int caretIndex = textarea.getCaretPosition(); int index = calcLineStart(caretIndex - 1, contents); //System.out.println("line start " + (int) contents[index]); index -= 2; // step over the newline //System.out.println((int) contents[index]); boolean onlySpaces = true; while (index > 0) { if (contents[index] == 10) { if (onlySpaces) { index++; break; } else { onlySpaces = true; // reset } } else if (contents[index] != ' ') { onlySpaces = false; } index--; } // if the first char, index will be -2 if (index < 0) index = 0; if ((event.getModifiers() & KeyEvent.SHIFT_MASK) != 0) { textarea.setSelectionStart(caretIndex); textarea.setSelectionEnd(index); } else { textarea.setCaretPosition(index); } event.consume(); return true; } else if ((code == KeyEvent.VK_DOWN) && ((event.getModifiers() & KeyEvent.CTRL_MASK) != 0)) { char contents[] = textarea.getText().toCharArray(); int caretIndex = textarea.getCaretPosition(); int index = caretIndex; int lineStart = 0; boolean onlySpaces = false; // don't count this line while (index < contents.length) { if (contents[index] == 10) { if (onlySpaces) { index = lineStart; // this is it break; } else { lineStart = index + 1; onlySpaces = true; // reset } } else if (contents[index] != ' ') { onlySpaces = false; } index++; } // if the first char, index will be -2 //if (index < 0) index = 0; //textarea.setSelectionStart(index); //textarea.setSelectionEnd(index); if ((event.getModifiers() & KeyEvent.SHIFT_MASK) != 0) { textarea.setSelectionStart(caretIndex); textarea.setSelectionEnd(index); } else { textarea.setCaretPosition(index); } event.consume(); return true; } switch ((int) c) { case 9: // expand tabs if (tabsExpand) { textarea.setSelectedText(tabString); event.consume(); return true; } else if (tabsIndent) { // this code is incomplete // if this brace is the only thing on the line, outdent //char contents[] = getCleanedContents(); char contents[] = textarea.getText().toCharArray(); // index to the character to the left of the caret int prevCharIndex = textarea.getCaretPosition() - 1; // now find the start of this line int lineStart = calcLineStart(prevCharIndex, contents); int lineEnd = lineStart; while ((lineEnd < contents.length - 1) && (contents[lineEnd] != 10)) { lineEnd++; } // get the number of braces, to determine whether this is an indent int braceBalance = 0; int index = lineStart; while ((index < contents.length) && (contents[index] != 10)) { if (contents[index] == '{') { braceBalance++; } else if (contents[index] == '}') { braceBalance--; } index++; } // if it's a starting indent, need to ignore it, so lineStart // will be the counting point. but if there's a closing indent, // then the lineEnd should be used. int where = (braceBalance > 0) ? lineStart : lineEnd; int indent = calcBraceIndent(where, contents); if (indent == -1) { // no braces to speak of, do nothing indent = 0; } else { indent += tabSize; } // and the number of spaces it has int spaceCount = calcSpaceCount(prevCharIndex, contents); textarea.setSelectionStart(lineStart); textarea.setSelectionEnd(lineStart + spaceCount); textarea.setSelectedText(Editor.EMPTY.substring(0, indent)); event.consume(); return true; } break; case 10: // auto-indent case 13: if (autoIndent) { char contents[] = textarea.getText().toCharArray(); // this is the previous character // (i.e. when you hit return, it'll be the last character // just before where the newline will be inserted) int origIndex = textarea.getCaretPosition() - 1; // NOTE all this cursing about CRLF stuff is probably moot // NOTE since the switch to JEditTextArea, which seems to use // NOTE only LFs internally (thank god). disabling for 0099. // walk through the array to the current caret position, // and count how many weirdo windows line endings there are, // which would be throwing off the caret position number /* int offset = 0; int realIndex = origIndex; for (int i = 0; i < realIndex-1; i++) { if ((contents[i] == 13) && (contents[i+1] == 10)) { offset++; realIndex++; } } // back up until \r \r\n or \n.. @#($* cross platform //System.out.println(origIndex + " offset = " + offset); origIndex += offset; // ARGH!#(* WINDOWS#@($* */ int spaceCount = calcSpaceCount(origIndex, contents); //int origCount = spaceCount; // now before inserting this many spaces, walk forward from // the caret position, so that the number of spaces aren't // just being duplicated again int index = origIndex + 1; int extraCount = 0; while ((index < contents.length) && (contents[index] == ' ')) { //spaceCount--; extraCount++; index++; } // hitting return on a line with spaces *after* the caret // can cause trouble. for simplicity's sake, just ignore this case. //if (spaceCount < 0) spaceCount = origCount; if (spaceCount - extraCount > 0) { spaceCount -= extraCount; } // if the last character was a left curly brace, then indent if (origIndex != -1) { if (contents[origIndex] == '{') { spaceCount += tabSize; } } String insertion = "\n" + Editor.EMPTY.substring(0, spaceCount); textarea.setSelectedText(insertion); // mark this event as already handled event.consume(); return true; } break; case '}': if (autoIndent) { // first remove anything that was there (in case this multiple // characters are selected, so that it's not in the way of the // spaces for the auto-indent if (textarea.getSelectionStart() != textarea.getSelectionEnd()) { textarea.setSelectedText(""); } // if this brace is the only thing on the line, outdent char contents[] = textarea.getText().toCharArray(); // index to the character to the left of the caret int prevCharIndex = textarea.getCaretPosition() - 1; // backup from the current caret position to the last newline, // checking for anything besides whitespace along the way. // if there's something besides whitespace, exit without // messing any sort of indenting. int index = prevCharIndex; boolean finished = false; while ((index != -1) && (!finished)) { if (contents[index] == 10) { finished = true; index++; } else if (contents[index] != ' ') { // don't do anything, this line has other stuff on it return false; } else { index--; } } if (!finished) return false; // brace with no start int lineStartIndex = index; /* // now that we know things are ok to be indented, walk // backwards to the last { to see how far its line is indented. // this isn't perfect cuz it'll pick up commented areas, // but that's not really a big deal and can be fixed when // this is all given a more complete (proper) solution. index = prevCharIndex; int braceDepth = 1; finished = false; while ((index != -1) && (!finished)) { if (contents[index] == '}') { // aww crap, this means we're one deeper // and will have to find one more extra { braceDepth++; index--; } else if (contents[index] == '{') { braceDepth--; if (braceDepth == 0) { finished = true; } // otherwise just teasing, keep going.. } else { index--; } } // never found a proper brace, be safe and don't do anything if (!finished) return false; // check how many spaces on the line with the matching open brace int pairedSpaceCount = calcSpaceCount(index, contents); //System.out.println(pairedSpaceCount); */ int pairedSpaceCount = calcBraceIndent(prevCharIndex, contents); //, 1); if (pairedSpaceCount == -1) return false; /* // now walk forward and figure out how many spaces there are while ((index < contents.length) && (index >= 0) && (contents[index++] == ' ')) { spaceCount++; } */ // number of spaces found on this line //int newSpaceCount = Math.max(0, spaceCount - tabSize); // number of spaces on this current line //int spaceCount = calcSpaces(caretIndex, contents); //System.out.println("spaces is " + spaceCount); //String insertion = "\n" + Editor.EMPTY.substring(0, spaceCount); //int differential = newSpaceCount - spaceCount; //System.out.println("diff is " + differential); //int newStart = textarea.getSelectionStart() + differential; //textarea.setSelectionStart(newStart); //textarea.setSelectedText("}"); textarea.setSelectionStart(lineStartIndex); textarea.setSelectedText(Editor.EMPTY.substring(0, pairedSpaceCount)); // mark this event as already handled event.consume(); return true; } break; } return false; } /** * Return the index for the first character on this line. */ protected int calcLineStart(int index, char contents[]) { // backup from the current caret position to the last newline, // so that we can figure out how far this line was indented int spaceCount = 0; boolean finished = false; while ((index != -1) && (!finished)) { if ((contents[index] == 10) || (contents[index] == 13)) { finished = true; //index++; // maybe ? } else { index--; // new } } // add one because index is either -1 (the start of the document) // or it's the newline character for the previous line return index + 1; } /** * Calculate the number of spaces on this line. */ protected int calcSpaceCount(int index, char contents[]) { index = calcLineStart(index, contents); int spaceCount = 0; // now walk forward and figure out how many spaces there are while ((index < contents.length) && (index >= 0) && (contents[index++] == ' ')) { spaceCount++; } return spaceCount; } /** * Walk back from 'index' until the brace that seems to be * the beginning of the current block, and return the number of * spaces found on that line. */ protected int calcBraceIndent(int index, char contents[]) { // now that we know things are ok to be indented, walk // backwards to the last { to see how far its line is indented. // this isn't perfect cuz it'll pick up commented areas, // but that's not really a big deal and can be fixed when // this is all given a more complete (proper) solution. int braceDepth = 1; boolean finished = false; while ((index != -1) && (!finished)) { if (contents[index] == '}') { // aww crap, this means we're one deeper // and will have to find one more extra { braceDepth++; //if (braceDepth == 0) { //finished = true; //} index--; } else if (contents[index] == '{') { braceDepth--; if (braceDepth == 0) { finished = true; } index--; } else { index--; } } // never found a proper brace, be safe and don't do anything if (!finished) return -1; // check how many spaces on the line with the matching open brace //int pairedSpaceCount = calcSpaceCount(index, contents); //System.out.println(pairedSpaceCount); return calcSpaceCount(index, contents); } /** * Get the character array and blank out the commented areas. * This hasn't yet been tested, the plan was to make auto-indent * less gullible (it gets fooled by braces that are commented out). */ protected char[] getCleanedContents() { char c[] = textarea.getText().toCharArray(); int index = 0; while (index < c.length - 1) { if ((c[index] == '/') && (c[index+1] == '*')) { c[index++] = 0; c[index++] = 0; while ((index < c.length - 1) && !((c[index] == '*') && (c[index+1] == '/'))) { c[index++] = 0; } } else if ((c[index] == '/') && (c[index+1] == '/')) { // clear out until the end of the line while ((index < c.length) && (c[index] != 10)) { c[index++] = 0; } if (index != c.length) { index++; // skip over the newline } } } return c; } /* protected char[] getCleanedContents() { char c[] = textarea.getText().toCharArray(); boolean insideMulti; // multi-line comment boolean insideSingle; // single line double slash //for (int i = 0; i < c.length - 1; i++) { int index = 0; while (index < c.length - 1) { if (insideMulti && (c[index] == '*') && (c[index+1] == '/')) { insideMulti = false; index += 2; } else if ((c[index] == '/') && (c[index+1] == '*')) { insideMulti = true; index += 2; } else if ((c[index] == '/') && (c[index+1] == '/')) { // clear out until the end of the line while (c[index] != 10) { c[index++] = 0; } index++; } } } */ }