change folder structure and control files to newest concept

This commit is contained in:
Daniel Stock
2018-09-20 16:38:35 +02:00
parent 224fd33794
commit e25c30f942
2747 changed files with 21 additions and 80 deletions
@@ -0,0 +1,130 @@
package antlr;
/* ANTLR Translator Generator
* Project led by Terence Parr at http://www.jGuru.com
* Software rights: http://www.antlr.org/RIGHTS.html
*
* $Id$
*/
import java.io.*;
import antlr.collections.*;
/** A CommonAST whose initialization copies hidden token
* information from the Token used to create a node.
*/
public class ExtendedCommonASTWithHiddenTokens
extends CommonASTWithHiddenTokens {
public ExtendedCommonASTWithHiddenTokens() {
super();
}
public ExtendedCommonASTWithHiddenTokens(Token tok) {
super(tok);
}
public void initialize(AST ast) {
ExtendedCommonASTWithHiddenTokens a =
(ExtendedCommonASTWithHiddenTokens)ast;
super.initialize(a);
hiddenBefore = a.getHiddenBefore();
hiddenAfter = a.getHiddenAfter();
}
public String getHiddenAfterString() {
CommonHiddenStreamToken t;
StringBuilder hiddenAfterString = new StringBuilder(100);
for ( t = hiddenAfter ; t != null ; t = t.getHiddenAfter() ) {
hiddenAfterString.append(t.getText());
}
return hiddenAfterString.toString();
}
public String getHiddenBeforeString() {
antlr.CommonHiddenStreamToken
child = null,
parent = hiddenBefore;
// if there aren't any hidden tokens here, quietly return
//
if (parent == null) {
return "";
}
// traverse back to the head of the list of tokens before this node
do {
child = parent;
parent = child.getHiddenBefore();
} while (parent != null);
// dump that list
StringBuilder hiddenBeforeString = new StringBuilder(100);
for ( CommonHiddenStreamToken t = child; t != null ;
t = t.getHiddenAfter() ) {
hiddenBeforeString.append(t.getText());
}
return hiddenBeforeString.toString();
}
public void xmlSerializeNode(Writer out) throws IOException {
StringBuilder sb = new StringBuilder(100);
sb.append("<");
sb.append(getClass().getName() + " ");
sb.append("hiddenBeforeString=\"" +
encode(getHiddenBeforeString()) +
"\" text=\"" + encode(getText()) + "\" type=\"" +
getType() + "\" hiddenAfterString=\"" +
encode(getHiddenAfterString()) + "\"/>");
out.write(sb.toString());
}
public void xmlSerializeRootOpen(Writer out) throws IOException {
StringBuilder sb = new StringBuilder(100);
sb.append("<");
sb.append(getClass().getName() + " ");
sb.append("hiddenBeforeString=\"" +
encode(getHiddenBeforeString()) +
"\" text=\"" + encode(getText()) + "\" type=\"" +
getType() + "\" hiddenAfterString=\"" +
encode(getHiddenAfterString()) + "\">\n");
out.write(sb.toString());
}
public void xmlSerializeRootClose(Writer out)
throws IOException {
out.write("</" + getClass().getName() + ">\n");
}
public void xmlSerialize(Writer out) throws IOException {
// print out this node and all siblings
for (AST node = this;
node != null;
node = node.getNextSibling()) {
if (node.getFirstChild() == null) {
// print guts (class name, attributes)
((BaseAST)node).xmlSerializeNode(out);
}
else {
((BaseAST)node).xmlSerializeRootOpen(out);
// print children
((BaseAST)node.getFirstChild()).xmlSerialize(out);
// print end tag
((BaseAST)node).xmlSerializeRootClose(out);
}
}
}
}
@@ -0,0 +1,219 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
package antlr;
import antlr.collections.impl.BitSet;
/**
* This class provides TokenStreamHiddenTokenFilters with the concept of
* tokens which can be copied so that they are seen by both the hidden token
* stream as well as the parser itself. This is useful when one wants to use
* an existing parser (like the Java parser included with ANTLR) that throws
* away some tokens to create a parse tree which can be used to spit out
* a copy of the code with only minor modifications.
*
* This code is partially derived from the public domain ANLTR TokenStream
*/
public class TokenStreamCopyingHiddenTokenFilter
extends TokenStreamHiddenTokenFilter
implements TokenStream {
protected BitSet copyMask;
CommonHiddenStreamToken hiddenCopy = null;
public TokenStreamCopyingHiddenTokenFilter(TokenStream input) {
super(input);
copyMask = new BitSet();
}
/**
* Indicate that all tokens of type tokenType should be copied. The copy
* is put in the stream of hidden tokens, and the original is returned in the
* stream of normal tokens.
*
* @param tokenType integer representing the token type to copied
*/
public void copy(int tokenType) {
copyMask.add(tokenType);
}
/**
* Create a clone of the important parts of the given token. Note that this
* does NOT copy the hiddenBefore and hiddenAfter fields.
*
* @param t token to partially clone
* @return newly created partial clone
*/
public CommonHiddenStreamToken partialCloneToken(CommonHiddenStreamToken t) {
CommonHiddenStreamToken u = new CommonHiddenStreamToken(t.getType(),
t.getText());
u.setColumn(t.getColumn());
u.setLine(t.getLine());
u.setFilename(t.getFilename());
return u;
}
public void linkAndCopyToken(CommonHiddenStreamToken prev,
CommonHiddenStreamToken monitored) {
// create a copy of the token in the lookahead for use as hidden token
hiddenCopy = partialCloneToken(LA(1));
// attach copy to the previous token, whether hidden or monitored
prev.setHiddenAfter(hiddenCopy);
// if previous token was hidden, set the hiddenBefore pointer of the
// copy to point back to it
if (prev != monitored) {
hiddenCopy.setHiddenBefore(prev);
}
// we don't want the non-hidden copy to link back to the hidden
// copy on the next pass through this function, so we leave
// lastHiddenToken alone
//System.err.println("hidden copy: " + hiddenCopy.toString());
return;
}
private void consumeFirst() throws TokenStreamException {
consume(); // get first token of input stream
// Handle situation where hidden or discarded tokens
// appear first in input stream
CommonHiddenStreamToken p=null;
// while hidden, copied, or discarded scarf tokens
while ( hideMask.member(LA(1).getType()) ||
discardMask.member(LA(1).getType()) ||
copyMask.member(LA(1).getType()) ) {
// if we've hit one of the tokens that needs to be copied, we copy it
// and then break out of the loop, because the parser needs to see it
// too
//
if (copyMask.member(LA(1).getType())) {
// copy the token in the lookahead
hiddenCopy = partialCloneToken(LA(1));
// if there's an existing token before this, link that and the
// copy together
if (p != null) {
p.setHiddenAfter(hiddenCopy);
hiddenCopy.setHiddenBefore(p); // double-link
}
lastHiddenToken = hiddenCopy;
if (firstHidden == null) {
firstHidden = hiddenCopy;
}
// we don't want to consume this token, because it also needs to
// be passed through to the parser, so break out of the while look
// entirely
//
break;
} else if (hideMask.member(LA(1).getType())) {
if (p != null) {
p.setHiddenAfter(LA(1));
LA(1).setHiddenBefore(p); // double-link
}
p = LA(1);
lastHiddenToken = p;
if (firstHidden == null) {
firstHidden = p; // record hidden token if first
}
}
consume();
}
}
/** Return the next monitored token.
* Test the token following the monitored token.
* If following is another monitored token, save it
* for the next invocation of nextToken (like a single
* lookahead token) and return it then.
* If following is unmonitored, nondiscarded (hidden)
* channel token, add it to the monitored token.
*
* Note: EOF must be a monitored Token.
*/
public Token nextToken() throws TokenStreamException {
// handle an initial condition; don't want to get lookahead
// token of this splitter until first call to nextToken
if (LA(1) == null) {
consumeFirst();
}
//System.err.println();
// we always consume hidden tokens after monitored, thus,
// upon entry LA(1) is a monitored token.
CommonHiddenStreamToken monitored = LA(1);
// point to hidden tokens found during last invocation
monitored.setHiddenBefore(lastHiddenToken);
lastHiddenToken = null;
// Look for hidden tokens, hook them into list emanating
// from the monitored tokens.
consume();
CommonHiddenStreamToken prev = monitored;
// deal with as many not-purely-monitored tokens as possible
while ( hideMask.member(LA(1).getType()) ||
discardMask.member(LA(1).getType()) ||
copyMask.member(LA(1).getType()) ) {
if (copyMask.member(LA(1).getType())) {
// copy the token and link it backwards
if (hiddenCopy != null) {
linkAndCopyToken(hiddenCopy, monitored);
} else {
linkAndCopyToken(prev, monitored);
}
// we now need to parse it as a monitored token, so we return, which
// avoids the consume() call at the end of this loop. the next call
// will parse it as a monitored token.
//System.err.println("returned: " + monitored.toString());
return monitored;
} else if (hideMask.member(LA(1).getType())) {
// attach the hidden token to the monitored in a chain
// link forwards
prev.setHiddenAfter(LA(1));
// link backwards
if (prev != monitored) { //hidden cannot point to monitored tokens
LA(1).setHiddenBefore(prev);
} else if (hiddenCopy != null) {
hiddenCopy.setHiddenAfter(LA(1));
LA(1).setHiddenBefore(hiddenCopy);
hiddenCopy = null;
}
//System.err.println("hidden: " + prev.getHiddenAfter().toString() + "\" after: " + prev.toString());
prev = lastHiddenToken = LA(1);
}
consume();
}
// remember the last hidden token for next time around
if (hiddenCopy != null) {
lastHiddenToken = hiddenCopy;
hiddenCopy = null;
}
//System.err.println("returned: " + monitored.toString());
return monitored;
}
}
@@ -0,0 +1,860 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Original Copyright (c) 1997, 1998 Van Di-Han HO. All Rights Reserved.
Updates Copyright (c) 2001 Jason Pell.
Further updates Copyright (c) 2003 Martin Gomez, Ateneo de Manila University
Additional updates Copyright (c) 2005-10 Ben Fry and Casey Reas
Even more updates Copyright (c) 2014 George Bateman
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, version 2.
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.java;
import java.util.Stack;
import java.util.regex.Pattern;
import processing.app.Formatter;
import processing.app.Preferences;
import processing.core.PApplet;
/**
* Handler for dealing with auto format.
* Contributed by Martin Gomez, additional bug fixes by Ben Fry.
* Additional fixes by Jonathan Feinberg in March 2010.
* <p/>
* After some further digging, this code in fact appears to be a modified
* version of Jason Pell's GPLed "Java Beautifier" class found
* <a href="http://web.archive.org/web/20091018220353/http://geocities.com/jasonpell/programs.html">here</a>,
* which is itself based on code from Van Di-Han Ho from
* <a href="http://web.archive.org/web/20091027043013/http://www.geocities.com/~starkville/vancbj_idx.html">here</a>.
* [Ben Fry, August 2009]
*/
public class AutoFormat implements Formatter {
private char[] chars;
private final StringBuilder buf = new StringBuilder();
private final StringBuilder result = new StringBuilder();
/** The number of spaces in one indent. Constant. */
private int indentValue;
/** Set when the end of the chars array is reached. */
private boolean EOF;
private boolean inStatementFlag; // in a line of code
private boolean overflowFlag; // line overrunning?
private boolean startFlag; // No buf has yet been writen to this line.
private boolean if_flg;
private boolean elseFlag;
/** -1 if not in array or if just after it, otherwise increases from 0. */
private int arrayLevel;
private int arrayIndent; // Lowest value of the above for this line.
/** Number of ? entered without exiting at : of a?b:c structures. */
private int conditionalLevel;
private int[][] sp_flg;
private boolean[][] s_ind;
private int if_lev;
/** chars[pos] is where we're at. */
private int pos;
private int level;
/** Number of curly brackets entered and not exited,
excluding arrays. */
private int curlyLvl;
/** Number of parentheses entered and not exited. */
private int parenLevel;
private boolean[] ind;
private int[] p_flg;
private int[][] s_tabs;
/** At every {, this has a true pushed if it's a do-while,
and a false otherwise. It is then popped at }. */
private Stack<Boolean> doWhileFlags;
/** At every (, this has a true pushed for if, while, or for,
and a false otherwise. Popped at ). */
private Stack<Boolean> ifWhileForFlags;
private boolean jdoc_flag;
/** The number of times to indent at a given point */
private int tabs;
/** The last non-space seen by nextChar(). */
private char lastNonWhitespace = 0;
private void handleMultiLineComment() {
final boolean savedStartFlag = startFlag;
buf.append(nextChar()); // So /*/ isn't self-closing.
for (char ch = nextChar(); !EOF; ch = nextChar()) {
buf.append(ch);
while (ch != '/' && !EOF) {
if (ch == '\n') {
writeIndentedComment();
startFlag = true;
}
buf.append(ch = nextChar());
}
if (buf.length() >= 2 && buf.charAt(buf.length() - 2) == '*') {
jdoc_flag = false;
break;
}
}
writeIndentedComment();
startFlag = savedStartFlag;
jdoc_flag = false;
return;
}
/**
* Pumps nextChar into buf until \n or EOF, then calls
* writeIndentedLine() and sets startFlag to true.
*/
private void handleSingleLineComment() {
char ch = nextChar();
while (ch != '\n' && !EOF) {
buf.append(ch);
ch = nextChar();
}
writeIndentedLine();
startFlag = true;
}
private void writeIndentedLine() {
if (buf.length() == 0) {
if (startFlag) startFlag = elseFlag = false;
return;
}
if (startFlag) {
// Indent suppressed at eg. if<nl>{ and when
// buf is close-brackets only followed by ';'.
boolean indentMore = !buf.toString().matches("[\\s\\]\\}\\)]+;")
&& (buf.charAt(0) != '{' || arrayLevel >= 0)
&& overflowFlag;
if (indentMore) {
tabs++;
if (arrayIndent > 0) tabs += arrayIndent;
}
printIndentation();
startFlag = false;
if (indentMore) {
tabs--;
if (arrayIndent > 0) tabs -= arrayIndent;
}
}
if (lastNonSpaceChar() == '}' && bufStarts("else")) {
result.append(' ');
}
if (elseFlag) {
if (lastNonSpaceChar() == '}') {
trimRight(result);
result.append(' ');
}
elseFlag = false;
}
// If we're still in a statement at \n, that's overflow.
overflowFlag = inStatementFlag;
arrayIndent = arrayLevel;
result.append(buf);
buf.setLength(0);
}
/**
* @return the last character in <tt>result</tt> not ' ' or '\n'.
*/
private char lastNonSpaceChar() {
for (int i = result.length() - 1; i >= 0; i--) {
char chI = result.charAt(i);
if (chI != ' ' && chI != '\n') return chI;
}
return 0;
}
/**
* Called by handleMultilineComment.<br />
* Sets jdoc_flag if at the start of a doc comment.
* Sends buf to result with proper indents, then clears buf.<br />
* Does nothing if buf is empty.
*/
private void writeIndentedComment() {
if (buf.length() == 0) return;
int firstNonSpace = 0;
while (buf.charAt(firstNonSpace) == ' ') firstNonSpace++;
if (lookup_com("/**")) jdoc_flag = true;
if (startFlag) printIndentation();
if (buf.charAt(firstNonSpace) == '/' && buf.charAt(firstNonSpace+1) == '*') {
if (startFlag && lastNonWhitespace != ';') {
result.append(buf.substring(firstNonSpace));
} else {
result.append(buf);
}
} else {
if (buf.charAt(firstNonSpace) == '*' || !jdoc_flag) {
result.append(" " + buf.substring(firstNonSpace));
} else {
result.append(" * " + buf.substring(firstNonSpace));
}
}
buf.setLength(0);
}
/**
* Makes tabs &gt;= 0 and appends <tt>tabs*indentValue</tt>
* spaces to result.
*/
private void printIndentation() {
if (tabs <= 0) {
tabs = 0;
return;
}
final int spaces = tabs * indentValue;
for (int i = 0; i < spaces; i++) {
result.append(' ');
}
}
/**
* @return <tt>chars[pos+1]</tt> or '\0' if out-of-bounds.
*/
private char peek() {
return (pos + 1 >= chars.length) ? 0 : chars[pos + 1];
}
/**
* Sets pos to the position of the next character that is not ' '
* in chars. If chars[pos] != ' ' already, it will still move on.
* Then sets EOF if pos has reached the end, or reverses pos by 1 if it
* has not.
* <br/> Does nothing if EOF.
* @param allWsp (boolean) Eat newlines too (all of Character.isWhiteSpace()).
*/
private void advanceToNonSpace(boolean allWsp) {
if (EOF) return;
if (allWsp) {
do {
pos++;
} while (pos < chars.length && Character.isWhitespace(chars[pos]));
} else {
do {
pos++;
} while (pos < chars.length && chars[pos] == ' ');
}
if (pos == chars.length - 1) {
EOF = true;
} else {
pos--; // reset for nextChar()
}
}
/**
* Increments pos, sets EOF if needed, and returns the new
* chars[pos] or zero if out-of-bounds.
* Sets lastNonWhitespace if chars[pos] isn't whitespace.
* Does nothing and returns zero if already at EOF.
*/
private char nextChar() {
if (EOF) return 0;
pos++;
if (pos >= chars.length - 1) EOF = true;
if (pos >= chars.length) return 0;
char retVal = chars[pos];
if (!Character.isWhitespace(retVal)) lastNonWhitespace = retVal;
return retVal;
}
/**
* Called after else.
*/
private void gotElse() {
tabs = s_tabs[curlyLvl][if_lev];
p_flg[level] = sp_flg[curlyLvl][if_lev];
ind[level] = s_ind[curlyLvl][if_lev];
if_flg = true;
// We can't expect else to be followed by a semicolon, so must
// end the statemant manually.
inStatementFlag = false;
}
/**
* Pump any '\t' and ' ' to buf, handle any following comment,
* and if the next character is '\n', discard it.
* @return Whether a '\n' was found and discarded.
*/
private boolean readForNewLine() {
final int savedTabs = tabs;
char c = peek();
while (!EOF && (c == '\t' || c == ' ')) {
buf.append(nextChar());
c = peek();
}
if (c == '/') {
buf.append(nextChar());
c = peek();
if (c == '*') {
buf.append(nextChar());
handleMultiLineComment();
} else if (c == '/') {
buf.append(nextChar());
handleSingleLineComment();
return true;
}
}
c = peek();
if (c == '\n') {
// eat it
nextChar();
tabs = savedTabs;
return true;
}
return false;
}
/**
* @return last non-wsp in result+buf, or 0 on error.
*/
private char prevNonWhitespace() {
StringBuffer tot = new StringBuffer();
tot.append(result);
tot.append(buf);
for (int i = tot.length()-1; i >= 0; i--) {
if (!Character.isWhitespace(tot.charAt(i)))
return tot.charAt(i);
}
return 0;
}
/**
* Sees if buf is of the form [optional whitespace][keyword][optional anything].
* It won't allow keyword to be directly followed by an alphanumeric, _, or &amp;.
* Will be different if keyword contains regex codes.
*/
private boolean bufStarts(final String keyword) {
return Pattern.matches("^\\s*" + keyword + "(?![a-zA-Z0-9_&]).*$", buf);
}
/**
* Sees if buf is of the form [optional anything][keyword][optional whitespace].
* It won't allow keyword to be directly preceded by an alphanumeric, _, or &amp;.
* Will be different if keyword contains regex codes.
*/
private boolean bufEnds(final String keyword) {
return Pattern.matches("^.*(?<![a-zA-Z0-9_&])" + keyword + "\\s*$", buf);
}
/**
* Allows you to increase if_lev safely. Enlarges related arrays where needed;
* does not change if_lev itself.
*/
private void if_levSafe() {
if (s_tabs[0].length <= if_lev) {
for (int i = 0; i < s_tabs.length; i++) {
s_tabs[i] = PApplet.expand(s_tabs[i]);
}
}
if (sp_flg[0].length <= if_lev) {
for (int i = 0; i < sp_flg.length; i++) {
sp_flg[i] = PApplet.expand(sp_flg[i]);
}
}
if (s_ind[0].length <= if_lev) {
for (int i = 0; i < s_ind.length; i++) {
s_ind[i] = PApplet.expand(s_ind[i]);
}
}
}
/**
* Sees if buf is of the form [optional whitespace][keyword][optional anything].
* It *will* allow keyword to be directly followed by an alphanumeric, _, or &amp;.
* Will be different if keyword contains regex codes (except *, which is fine).
*/
private boolean lookup_com(final String keyword) {
final String regex = "^\\s*" + keyword.replace("*", "\\*") + ".*$";
return Pattern.matches(regex, buf);
}
/**
* Takes all whitespace off the end of its argument.
*/
static private void trimRight(final StringBuilder sb) {
while (sb.length() >= 1 && Character.isWhitespace(sb.charAt(sb.length() - 1))) {
sb.setLength(sb.length() - 1);
}
}
/** Entry point */
public String format(final String source) {
final String normalizedText = source.replaceAll("\r", "");
String cleanText = normalizedText;
if (!normalizedText.endsWith("\n")) {
cleanText += "\n";
}
// Globals' description at top of file.
result.setLength(0);
indentValue = Preferences.getInteger("editor.tabs.size");
boolean forFlag = if_flg = false;
startFlag = true;
int forParenthLevel = 0;
conditionalLevel = parenLevel = curlyLvl = if_lev = level = 0;
tabs = 0;
jdoc_flag = inStatementFlag = overflowFlag = false;
pos = arrayLevel = -1;
int[] s_level = new int[10];
sp_flg = new int[20][10];
s_ind = new boolean[20][10];
int[] s_if_lev = new int[10]; // Stack
boolean[] s_if_flg = new boolean[10]; // Stack
ind = new boolean[10];
p_flg = new int[10];
s_tabs = new int[20][10];
doWhileFlags = new Stack<>();
ifWhileForFlags = new Stack<>();
chars = cleanText.toCharArray();
EOF = false; // set in nextChar() when EOF
while (!EOF) {
char c = nextChar();
switch (c) {
default:
inStatementFlag = true;
buf.append(c);
break;
case ',':
inStatementFlag = true;
trimRight(buf);
buf.append(", ");
advanceToNonSpace(false);
break;
case ' ':
case '\t':
elseFlag = bufEnds("else");
if (elseFlag) {
gotElse();
if (!startFlag || buf.length() > 0) {
buf.append(c);
}
writeIndentedLine();
startFlag = false;
break;
}
// Only allow in the line, to nuke the old indent.
if (!startFlag || buf.length() > 0) buf.append(c);
break;
case '\n':
if (EOF) break;
elseFlag = bufEnds("else");
if (elseFlag) gotElse();
if (lookup_com("//")) {
if (buf.charAt(buf.length() - 1) == '\n') {
buf.setLength(buf.length() - 1);
}
}
if (elseFlag) {
writeIndentedLine();
result.append("\n");
p_flg[level]++;
tabs++;
} else {
writeIndentedLine();
result.append("\n");
}
startFlag = true;
break;
case '{':
elseFlag = bufEnds("else");
if (elseFlag) gotElse();
doWhileFlags.push(Boolean.valueOf(bufEnds("do")));
char prevChar = prevNonWhitespace();
if (arrayLevel >= 0 || prevChar == '=' || prevChar == ']') {
// If we're already in an array (lvl >= 0), increment level.
// Otherwise, the presence of a = or ] indicates an array is starting
// and we should start counting (set lvl=0).
arrayLevel++;
buf.append(c);
break; // Nothing fancy.
}
inStatementFlag = false; // eg. class declaration ends
if (s_if_lev.length == curlyLvl) {
s_if_lev = PApplet.expand(s_if_lev);
s_if_flg = PApplet.expand(s_if_flg);
}
s_if_lev[curlyLvl] = if_lev;
s_if_flg[curlyLvl] = if_flg;
if_lev = 0;
if_flg = false;
curlyLvl++;
if (startFlag && p_flg[level] != 0) {
p_flg[level]--;
tabs--;
}
trimRight(buf);
if (buf.length() > 0 || (result.length() > 0 &&
!Character.isWhitespace(result.charAt(result.length() - 1)))) {
buf.append(" ");
}
buf.append(c);
writeIndentedLine();
readForNewLine();
writeIndentedLine();
result.append('\n');
tabs++;
startFlag = true;
if (p_flg[level] > 0) {
ind[level] = true;
level++;
s_level[level] = curlyLvl;
}
break;
case '}':
if (arrayLevel >= 0) {
// Even less fancy. Note that }s cannot end array behaviour;
// a semicolon is needed.
if (arrayLevel > 0) arrayLevel--;
if (arrayIndent > arrayLevel) arrayIndent = arrayLevel;
buf.append(c);
break;
}
// In a simple enum, there's not necessarily a `;` to end the statement.
inStatementFlag = false;
curlyLvl--;
if (curlyLvl < 0) {
curlyLvl = 0;
buf.append(c);
writeIndentedLine();
} else {
if_lev = s_if_lev[curlyLvl] - 1;
if (if_lev < 0) if_lev = 0;
if_levSafe();
if_flg = s_if_flg[curlyLvl];
trimRight(buf);
writeIndentedLine();
tabs--;
trimRight(result);
result.append('\n');
overflowFlag = false; // Would normally be done in writeIndentedLine.
printIndentation();
result.append(c);
if (peek() == ';') result.append(nextChar());
// doWhileFlags contains a TRUE if and only if the
// corresponding { was preceeded (bufEnds) by "do".
// Just checking for while would fail on if(){} while(){}.
if (doWhileFlags.empty() || !doWhileFlags.pop().booleanValue()
|| !new String(chars, pos+1, chars.length-pos-1).trim().startsWith("while")) {
readForNewLine();
writeIndentedLine();
result.append('\n');
startFlag = true;
} else {
// Correct spacing in "} while".
result.append(' ');
advanceToNonSpace(true);
startFlag = false;
}
if (curlyLvl < s_level[level] && level > 0) level--;
if (ind[level]) {
tabs -= p_flg[level];
p_flg[level] = 0;
ind[level] = false;
}
}
break;
case '"':
case '“':
case '”':
case '\'':
case '':
case '':
inStatementFlag = true;
char realQuote = c;
if (c == '“' || c == '”') realQuote = '"';
if (c == '' || c == '') realQuote = '\'';
buf.append(realQuote);
char otherQuote = c;
if (c == '“') otherQuote = '”';
if (c == '”') otherQuote = '“';
if (c == '') otherQuote = '';
if (c == '') otherQuote = '';
char cc = nextChar();
// In a proper string, all the quotes tested are c. In a curly-quoted
// string, there are three possible end quotes: c, its reverse, and
// the correct straight quote.
while (!EOF && cc != otherQuote && cc != realQuote && cc != c) {
buf.append(cc);
if (cc == '\\') {
buf.append(cc = nextChar());
}
// Syntax error: unterminated string. Leave \n in nextChar, so it
// feeds back into the loop.
if (peek() == '\n') break;
cc = nextChar();
}
if (cc == otherQuote || cc == realQuote || cc == c) {
buf.append(realQuote);
if (readForNewLine()) {
// push a newline into the stream
chars[pos--] = '\n';
}
} else {
// We've had a syntax error if the string wasn't terminated by EOL/
// EOF, just abandon this statement.
inStatementFlag = false;
}
break;
case ';':
if (forFlag) {
// This is like a comma.
trimRight(buf);
buf.append("; ");
// Not non-whitespace: allow \n.
advanceToNonSpace(false);
break;
}
buf.append(c);
inStatementFlag = false;
writeIndentedLine();
if (p_flg[level] > 0 && !ind[level]) {
tabs -= p_flg[level];
p_flg[level] = 0;
}
readForNewLine();
writeIndentedLine();
result.append("\n");
startFlag = true;
// Array behaviour ends at the end of a statement.
arrayLevel = -1;
if (if_lev > 0) {
if (if_flg) {
if_lev--;
if_flg = false;
} else {
if_lev = 0;
}
}
break;
case '\\':
buf.append(c);
buf.append(nextChar());
break;
case '?':
conditionalLevel++;
buf.append(c);
break;
case ':':
// Java 8 :: operator.
if (peek() == ':') {
result.append(c).append(nextChar());
break;
}
// End a?b:c structures.
else if (conditionalLevel > 0) {
conditionalLevel--;
buf.append(c);
break;
}
else if (forFlag) {
trimRight(buf);
buf.append(" : ");
// Not to non-whitespace: allow \n.
advanceToNonSpace(false);
break;
}
buf.append(c);
inStatementFlag = false;
arrayLevel = -1; // Unlikely to be needed; just in case.
// Same format for case, default, and other labels.
if (tabs > 0) {
tabs--;
writeIndentedLine();
tabs++;
} else {
writeIndentedLine();
}
readForNewLine();
writeIndentedLine();
result.append('\n');
startFlag = true;
break;
case '/':
final char next = peek();
if (next == '/') {
// call nextChar to move on.
buf.append(c).append(nextChar());
handleSingleLineComment();
result.append("\n");
} else if (next == '*') {
if (buf.length() > 0) {
writeIndentedLine();
}
buf.append(c).append(nextChar());
handleMultiLineComment();
} else {
buf.append(c);
}
break;
case ')':
parenLevel--;
// If we're further back than the start of a for loop, we've
// left it.
if (forFlag && forParenthLevel > parenLevel) forFlag = false;
if (parenLevel < 0) parenLevel = 0;
buf.append(c);
boolean wasIfEtc = !ifWhileForFlags.empty() && ifWhileForFlags.pop().booleanValue();
if (wasIfEtc) {
inStatementFlag = false;
arrayLevel = -1; // This is important as it allows arrays in if statements.
}
writeIndentedLine();
// Short-circuiting means readForNewLine is only called for if/while/for;
// this is important.
if (wasIfEtc && readForNewLine()) {
chars[pos] = '\n';
pos--; // Make nextChar() return the new '\n'.
if (parenLevel == 0) {
p_flg[level]++;
tabs++;
ind[level] = false;
}
}
break;
case '(':
final boolean isFor = bufEnds("for");
final boolean isIf = bufEnds("if");
if (isFor || isIf || bufEnds("while")) {
if (!Character.isWhitespace(buf.charAt(buf.length() - 1))) {
buf.append(' ');
}
ifWhileForFlags.push(true);
} else {
ifWhileForFlags.push(false);
}
buf.append(c);
parenLevel++;
// isFor says "Is it the start of a for?". If it is, we set forFlag and
// forParenthLevel. If it is not parenth_lvl was incremented above and
// that's it.
if (isFor && !forFlag) {
forParenthLevel = parenLevel;
forFlag = true;
} else if (isIf) {
writeIndentedLine();
s_tabs[curlyLvl][if_lev] = tabs;
sp_flg[curlyLvl][if_lev] = p_flg[level];
s_ind[curlyLvl][if_lev] = ind[level];
if_lev++;
if_levSafe();
if_flg = true;
}
} // end switch
} // end while not EOF
if (buf.length() > 0) writeIndentedLine();
final String formatted = result.toString();
return formatted.equals(cleanText) ? source : formatted;
}
}
@@ -0,0 +1,427 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 The Processing Foundation
Copyright (c) 2008-12 Ben Fry and Casey Reas
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.java;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import processing.app.Base;
import processing.app.Platform;
import processing.app.Preferences;
import processing.app.RunnerListener;
import processing.app.Sketch;
import processing.app.SketchException;
import processing.app.Util;
import processing.app.contrib.ModeContribution;
import processing.core.PApplet;
import processing.mode.java.runner.Runner;
/**
* Class to handle running Processing from the command line.
*
* @author fry
*/
public class Commander implements RunnerListener {
static final String helpArg = "--help";
// static final String preprocArg = "--preprocess";
static final String buildArg = "--build";
static final String runArg = "--run";
static final String presentArg = "--present";
static final String sketchArg = "--sketch=";
static final String forceArg = "--force";
static final String outputArg = "--output=";
static final String exportApplicationArg = "--export";
static final String noJavaArg = "--no-java";
static final String platformArg = "--platform=";
static final String bitsArg = "--bits=";
// static final String preferencesArg = "--preferences=";
static final int HELP = -1;
static final int PREPROCESS = 0;
static final int BUILD = 1;
static final int RUN = 2;
static final int PRESENT = 3;
// static final int EXPORT_APPLET = 4;
static final int EXPORT = 4;
Sketch sketch;
PrintStream systemOut;
PrintStream systemErr;
static public void main(String[] args) {
// Do this early so that error messages go to the console
Base.setCommandLine();
// init the platform so that prefs and other native code is ready to go
Platform.init();
// make sure a full JDK is installed
//Base.initRequirements();
// launch command line handler
new Commander(args);
}
public Commander(String[] args) {
String sketchPath = null;
File sketchFolder = null;
String pdePath = null; // path to the .pde file
String outputPath = null;
File outputFolder = null;
boolean outputSet = false; // set an output folder
boolean force = false; // replace that no good output folder
// String preferencesPath = null;
int platform = PApplet.platform; // default to this platform
// int platformBits = Base.getNativeBits();
int task = HELP;
boolean embedJava = true;
try {
if (Platform.isWindows()) {
// On Windows, it needs to use the default system encoding.
// https://github.com/processing/processing/issues/1633
systemOut = new PrintStream(System.out, true);
systemErr = new PrintStream(System.err, true);
} else {
// On OS X, the output goes as MacRoman or something else useless.
// http://code.google.com/p/processing/issues/detail?id=1418
// (Not sure about Linux, but this has worked since 2.0)
systemOut = new PrintStream(System.out, true, "UTF-8");
systemErr = new PrintStream(System.err, true, "UTF-8");
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
System.exit(1);
}
int argOffset = 0;
for (String arg : args) {
argOffset++;
if (arg.length() == 0) {
// ignore it, just the crappy shell script
} else if (arg.equals(helpArg)) {
// mode already set to HELP
// } else if (arg.equals(preprocArg)) {
// task = PREPROCESS;
} else if (arg.equals(buildArg)) {
task = BUILD;
break;
} else if (arg.equals(runArg)) {
task = RUN;
break;
} else if (arg.equals(presentArg)) {
task = PRESENT;
break;
} else if (arg.equals(exportApplicationArg)) {
task = EXPORT;
break;
} else if (arg.equals(noJavaArg)) {
embedJava = false;
} else if (arg.startsWith(platformArg)) {
// complainAndQuit("The --platform option has been removed from Processing 2.1.", false);
String platformStr = arg.substring(platformArg.length());
platform = Platform.getIndex(platformStr);
if (platform == -1) {
complainAndQuit(platformStr + " should instead be " +
"'windows', 'macosx', or 'linux'.", true);
}
} else if (arg.startsWith(bitsArg)) {
complainAndQuit("The --bits option has been removed.", false);
// String bitsStr = arg.substring(bitsArg.length());
// if (bitsStr.equals("32")) {
// platformBits = 32;
// } else if (bitsStr.equals("64")) {
// platformBits = 64;
// } else {
// complainAndQuit("Bits should be either 32 or 64, not " + bitsStr, true);
// }
} else if (arg.startsWith(sketchArg)) {
sketchPath = arg.substring(sketchArg.length());
sketchFolder = new File(sketchPath);
if (!sketchFolder.exists()) {
complainAndQuit(sketchFolder + " does not exist.", false);
}
File pdeFile = new File(sketchFolder, sketchFolder.getName() + ".pde");
if (!pdeFile.exists()) {
complainAndQuit("Not a valid sketch folder. " + pdeFile + " does not exist.", true);
}
pdePath = pdeFile.getAbsolutePath();
// } else if (arg.startsWith(preferencesArg)) {
// preferencesPath = arg.substring(preferencesArg.length());
} else if (arg.startsWith(outputArg)) {
outputSet = true;
outputPath = arg.substring(outputArg.length());
} else if (arg.equals(forceArg)) {
force = true;
} else {
complainAndQuit("I don't know anything about " + arg + ".", true);
}
}
String[] sketchArgs = PApplet.subset(args, argOffset);
// if ((outputPath == null) &&
// (task == PREPROCESS || task == BUILD ||
// task == RUN || task == PRESENT)) {
// complainAndQuit("An output path must be specified when using " +
// preprocArg + ", " + buildArg + ", " +
// runArg + ", or " + presentArg + ".");
// }
if (task == HELP) {
printCommandLine(systemOut);
System.exit(0);
}
if (outputSet) {
if (outputPath == null) {
complainAndQuit("An output path must be specified.", true);
}
outputFolder = new File(outputPath);
if (outputFolder.exists()) {
if (force) {
Util.removeDir(outputFolder);
} else {
complainAndQuit("The output folder already exists. " +
"Use --force to remove it.", false);
}
}
if (!outputFolder.mkdirs()) {
complainAndQuit("Could not create the output folder.", false);
}
}
// // run static initialization that grabs all the prefs
// // (also pass in a prefs path if that was specified)
// if (preferencesPath != null) {
// Preferences.init(preferencesPath);
// }
Preferences.init();
Base.locateSketchbookFolder();
if (sketchPath == null) {
complainAndQuit("No sketch path specified.", true);
// } else if (!pdePath.toLowerCase().endsWith(".pde")) {
// complainAndQuit("Sketch path must point to the main .pde file.", false);
} else {
if (outputSet) {
if (outputPath.equals(sketchPath)) {
complainAndQuit("The sketch path and output path cannot be identical.", false);
}
}
boolean success = false;
// JavaMode javaMode =
// new JavaMode(null, Base.getContentFile("modes/java"));
JavaMode javaMode = (JavaMode)
ModeContribution.load(null, Platform.getContentFile("modes/java"),
"processing.mode.java.JavaMode").getMode();
try {
sketch = new Sketch(pdePath, javaMode);
if (!outputSet) {
outputFolder = sketch.makeTempFolder();
}
if (task == BUILD || task == RUN || task == PRESENT) {
JavaBuild build = new JavaBuild(sketch);
File srcFolder = new File(outputFolder, "source");
String className = build.build(srcFolder, outputFolder, true);
// String className = build.build(sketchFolder, outputFolder, true);
if (className != null) {
success = true;
if (task == RUN || task == PRESENT) {
Runner runner = new Runner(build, this);
if (task == PRESENT) {
runner.present(sketchArgs);
} else {
runner.launch(sketchArgs);
}
success = !runner.vmReturnedError();
}
} else {
success = false;
}
} else if (task == EXPORT) {
if (outputPath == null) {
javaMode.handleExportApplication(sketch);
} else {
JavaBuild build = new JavaBuild(sketch);
build.build(true);
if (build != null) {
String variant = Platform.getVariant();
success = build.exportApplication(outputFolder, platform, variant, embedJava);
}
}
}
if (!success) { // error already printed
System.exit(1);
}
systemOut.println("Finished.");
System.exit(0);
} catch (SketchException re) {
statusError(re);
System.exit(1);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
}
public void statusNotice(String message) {
systemErr.println(message);
}
public void statusError(String message) {
systemErr.println(message);
}
public void statusError(Exception exception) {
if (exception instanceof SketchException) {
SketchException re = (SketchException) exception;
int codeIndex = re.getCodeIndex();
if (codeIndex != -1) {
// format the runner exception like emacs
//blah.java:2:10:2:13: Syntax Error: This is a big error message
// Emacs doesn't like the double line thing coming from Java
// https://github.com/processing/processing/issues/2158
String filename = sketch.getCode(codeIndex).getFileName();
int line = re.getCodeLine() + 1;
int column = re.getCodeColumn() + 1;
//if (column == -1) column = 0;
// TODO if column not specified, should just select the whole line.
// But what's the correct syntax for that?
systemErr.println(filename + ":" +
line + ":" + column + ":" +
line + ":" + column + ":" + " " + re.getMessage());
} else { // no line number, pass the trace along to the user
exception.printStackTrace();
}
} else {
exception.printStackTrace();
}
}
void complainAndQuit(String lastWords, boolean schoolEmFirst) {
if (schoolEmFirst) {
printCommandLine(systemErr);
}
systemErr.println(lastWords);
System.exit(1);
}
static void printCommandLine(PrintStream out) {
out.println();
out.println("Command line edition for Processing " + Base.getVersionName() + " (Java Mode)");
out.println();
out.println("--help Show this help text. Congratulations.");
out.println();
out.println("--sketch=<name> Specify the sketch folder (required)");
out.println("--output=<name> Specify the output folder (optional and");
out.println(" cannot be the same as the sketch folder.)");
out.println();
out.println("--force The sketch will not build if the output");
out.println(" folder already exists, because the contents");
out.println(" will be replaced. This option erases the");
out.println(" folder first. Use with extreme caution!");
out.println();
out.println("--build Preprocess and compile a sketch into .class files.");
out.println("--run Preprocess, compile, and run a sketch.");
out.println("--present Preprocess, compile, and run a sketch in presentation mode.");
out.println();
out.println("--export Export an application.");
out.println("--no-java Do not embed Java. Use at your own risk!");
out.println("--platform Specify the platform (export to application only).");
out.println(" Should be one of 'windows', 'macosx', or 'linux'.");
// out.println("--bits Must be specified if libraries are used that are");
// out.println(" 32- or 64-bit specific such as the OpenGL library.");
// out.println(" Otherwise specify 0 or leave it out.");
out.println();
out.println("The --build, --run, --present, or --export must be the final parameter");
out.println("passed to Processing. Arguments passed following one of those four will");
out.println("be passed through to the sketch itself, and therefore available to the");
out.println("sketch via the 'args' field. To pass options understood by PApplet.main(),");
out.println("write a custom main() method so that the preprocessor does not add one.");
out.println("https://github.com/processing/processing/wiki/Command-Line");
out.println();
}
@Override
public void startIndeterminate() {
}
@Override
public void stopIndeterminate() {
}
@Override
public void statusHalt() {
}
@Override
public boolean isHalted() {
return false;
}
}
@@ -0,0 +1,331 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2004-12 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.mode.java;
import processing.app.*;
import processing.app.ui.Editor;
import processing.core.*;
import java.io.*;
import java.lang.reflect.Method;
import java.util.HashMap;
//import org.eclipse.jdt.core.compiler.batch.BatchCompiler;
//import org.eclipse.jdt.core.compiler.CompilationProgress;
public class Compiler {
static HashMap<String, String> importSuggestions;
static {
importSuggestions = new HashMap<>();
importSuggestions.put("Arrays", "java.util.Arrays");
importSuggestions.put("Collections", "java.util.Collections");
importSuggestions.put("Date", "java.util.Date");
importSuggestions.put("Frame", "java.awt.Frame");
importSuggestions.put("Iterator", "java.util.Iterator");
}
/**
* Compile with ECJ. See http://j.mp/8paifz for documentation.
*
* @param sketch Sketch object to be compiled, used for placing exceptions
* @param buildPath Where the temporary files live and will be built from.
* @return true if successful.
* @throws SketchException Only if there's a problem. Only then.
*/
static public boolean compile(JavaBuild build) throws SketchException {
// This will be filled in if anyone gets angry
SketchException exception = null;
boolean success = false;
String baseCommand[] = new String[] {
"-g",
"-Xemacs",
//"-noExit", // not necessary for ecj
"-source", "1.7",
"-target", "1.7",
"-encoding", "utf8",
"-classpath", build.getClassPath(),
"-nowarn", // we're not currently interested in warnings (works in ecj)
"-d", build.getBinFolder().getAbsolutePath() // output the classes in the buildPath
};
//PApplet.println(baseCommand);
String[] sourceFiles = Util.listFiles(build.getSrcFolder(), false, ".java");
String[] command = PApplet.concat(baseCommand, sourceFiles);
//PApplet.println(command);
try {
// Load errors into a local StringBuilder
final StringBuilder errorBuffer = new StringBuilder();
// Create single method dummy writer class to slurp errors from ecj
Writer internalWriter = new Writer() {
public void write(char[] buf, int off, int len) {
errorBuffer.append(buf, off, len);
}
public void flush() { }
public void close() { }
};
// Wrap as a PrintWriter since that's what compile() wants
PrintWriter writer = new PrintWriter(internalWriter);
//result = com.sun.tools.javac.Main.compile(command, writer);
PrintWriter outWriter = new PrintWriter(System.out);
// Version that's not dynamically loaded
//CompilationProgress progress = null;
//success = BatchCompiler.compile(command, outWriter, writer, progress);
// Version that *is* dynamically loaded. First gets the mode class loader
// so that it can grab the compiler JAR files from it.
ClassLoader loader = build.mode.getClassLoader();
try {
Class<?> batchClass =
Class.forName("org.eclipse.jdt.core.compiler.batch.BatchCompiler", false, loader);
Class<?> progressClass =
Class.forName("org.eclipse.jdt.core.compiler.CompilationProgress", false, loader);
Class<?>[] compileArgs =
new Class<?>[] { String[].class, PrintWriter.class, PrintWriter.class, progressClass };
Method compileMethod = batchClass.getMethod("compile", compileArgs);
success = (Boolean)
compileMethod.invoke(null, new Object[] { command, outWriter, writer, null });
} catch (Exception e) {
e.printStackTrace();
throw new SketchException("Unknown error inside the compiler.");
}
// Close out the stream for good measure
writer.flush();
writer.close();
BufferedReader reader =
new BufferedReader(new StringReader(errorBuffer.toString()));
//System.err.println(errorBuffer.toString());
String line = null;
while ((line = reader.readLine()) != null) {
//System.out.println("got line " + line); // debug
// get first line, which contains file name, line number,
// and at least the first line of the error message
String errorFormat = "([\\w\\d_]+.java):(\\d+):\\s*(.*):\\s*(.*)\\s*";
String[] pieces = PApplet.match(line, errorFormat);
//PApplet.println(pieces);
// if it's something unexpected, die and print the mess to the console
if (pieces == null) {
exception = new SketchException("Cannot parse error text: " + line);
exception.hideStackTrace();
// Send out the rest of the error message to the console.
System.err.println(line);
while ((line = reader.readLine()) != null) {
System.err.println(line);
}
break;
}
// translate the java filename and line number into a un-preprocessed
// location inside a source file or tab in the environment.
String dotJavaFilename = pieces[1];
// Line numbers are 1-indexed from javac
int dotJavaLineIndex = PApplet.parseInt(pieces[2]) - 1;
String errorMessage = pieces[4];
exception = build.placeException(errorMessage,
dotJavaFilename,
dotJavaLineIndex);
if (exception == null) {
exception = new SketchException(errorMessage);
}
String[] parts = null;
if (errorMessage.startsWith("The import ") &&
errorMessage.endsWith("cannot be resolved")) {
// The import poo cannot be resolved
//import poo.shoe.blah.*;
//String what = errorMessage.substring("The import ".length());
String[] m = PApplet.match(errorMessage, "The import (.*) cannot be resolved");
//what = what.substring(0, what.indexOf(' '));
if (m != null) {
// System.out.println("'" + m[1] + "'");
if (m[1].equals("processing.xml")) {
exception.setMessage("processing.xml no longer exists, this code needs to be updated for 2.0.");
System.err.println("The processing.xml library has been replaced " +
"with a new 'XML' class that's built-in.");
handleCrustyCode();
} else {
exception.setMessage("The package " +
"\u201C" + m[1] + "\u201D" +
" does not exist. " +
"You might be missing a library.");
System.err.println("Libraries must be " +
"installed in a folder named 'libraries' " +
"inside the sketchbook folder " +
"(see the Preferences window).");
}
}
} else if (errorMessage.endsWith("cannot be resolved to a type")) {
// xxx cannot be resolved to a type
//xxx c;
String what = errorMessage.substring(0, errorMessage.indexOf(' '));
if (what.equals("BFont") ||
what.equals("BGraphics") ||
what.equals("BImage")) {
exception.setMessage(what + " has been replaced with P" + what.substring(1));
handleCrustyCode();
} else {
exception.setMessage("Cannot find a class or type " +
"named \u201C" + what + "\u201D");
String suggestion = importSuggestions.get(what);
if (suggestion != null) {
System.err.println("You may need to add \"import " + suggestion + ";\" to the top of your sketch.");
System.err.println("To make sketches more portable, imports that are not part of the Processing API were removed in Processing 2.");
System.err.println("See the changes page for more information: https://github.com/processing/processing/wiki/Changes");
}
}
} else if (errorMessage.endsWith("cannot be resolved")) {
// xxx cannot be resolved
//println(xxx);
String what = errorMessage.substring(0, errorMessage.indexOf(' '));
if (what.equals("LINE_LOOP") ||
what.equals("LINE_STRIP")) {
exception.setMessage("LINE_LOOP and LINE_STRIP are not available, " +
"please update your code.");
handleCrustyCode();
} else if (what.equals("framerate")) {
exception.setMessage("framerate should be changed to frameRate.");
handleCrustyCode();
} else if (what.equals("screen")) {
exception.setMessage("Change screen.width and screen.height to " +
"displayWidth and displayHeight.");
handleCrustyCode();
} else if (what.equals("screenWidth") ||
what.equals("screenHeight")) {
exception.setMessage("Change screenWidth and screenHeight to " +
"displayWidth and displayHeight.");
handleCrustyCode();
} else {
exception.setMessage("Cannot find anything " +
"named \u201C" + what + "\u201D");
}
} else if (errorMessage.startsWith("Duplicate")) {
// "Duplicate nested type xxx"
// "Duplicate local variable xxx"
} else if (null != (parts = PApplet.match(errorMessage,
"literal (\\S*) of type (\\S*) is out of range"))) {
if ("int".equals(parts[2])) {
exception.setMessage("The type int can't handle numbers that big. Try "
+ parts[1] + "L to upgrade to long.");
} else {
// I'd like to give an essay on BigInteger and BigDecimal, but
// this margin is too narrow to contain it.
exception.setMessage("Even the type " + parts[2] + " can't handle "
+ parts[1] + ". Research big numbers in Java.");
}
} else {
// The method xxx(String) is undefined for the type Temporary_XXXX_XXXX
//xxx("blah");
// The method xxx(String, int) is undefined for the type Temporary_XXXX_XXXX
//xxx("blah", 34);
// The method xxx(String, int) is undefined for the type PApplet
//PApplet.sub("ding");
String undefined =
"The method (\\S+\\(.*\\)) is undefined for the type (.*)";
parts = PApplet.match(errorMessage, undefined);
if (parts != null) {
if (parts[1].equals("framerate(int)")) {
exception.setMessage("framerate() no longer exists, use frameRate() instead.");
handleCrustyCode();
} else if (parts[1].equals("push()")) {
exception.setMessage("push() no longer exists, use pushMatrix() instead.");
handleCrustyCode();
} else if (parts[1].equals("pop()")) {
exception.setMessage("pop() no longer exists, use popMatrix() instead.");
handleCrustyCode();
} else {
String mess = "The function " + parts[1] + " does not exist.";
exception.setMessage(mess);
}
break;
}
}
if (exception != null) {
// The stack trace just shows that this happened inside the compiler,
// which is a red herring. Don't ever show it for compiler stuff.
exception.hideStackTrace();
break;
}
}
} catch (IOException e) {
String bigSigh = "Error while compiling. (" + e.getMessage() + ")";
exception = new SketchException(bigSigh);
e.printStackTrace();
success = false;
}
// In case there was something else.
if (exception != null) throw exception;
return success;
}
static protected void handleCrustyCode() {
System.err.println("This code needs to be updated " +
"for this version of Processing, " +
"please read the Changes page on the Wiki.");
Editor.showChanges();
}
protected int caretColumn(String caretLine) {
return caretLine.indexOf("^");
}
}
@@ -0,0 +1,421 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 The Processing Foundation
Copyright (c) 2004-12 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.mode.java;
import processing.app.Preferences;
import processing.app.Sketch;
import processing.app.syntax.*;
import processing.app.ui.Editor;
import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;
/**
* Filters key events for tab expansion/indent/etc. This is very old code
* that we'd love to replace with a smarter parser/formatter, rather than
* continuing to hack this class.
*/
public class JavaInputHandler extends PdeInputHandler {
/** ctrl-alt on windows and linux, cmd-alt on mac os x */
static final int CTRL_ALT = ActionEvent.ALT_MASK |
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
public JavaInputHandler(Editor editor) {
super(editor);
}
/**
* Intercepts key pressed events for JEditTextArea.
* <p/>
* Called by JEditTextArea inside processKeyEvent(). Note that this
* won't intercept actual characters, because those are fired on
* keyTyped().
* @return true if the event has been handled (to remove it from the queue)
*/
public boolean handlePressed(KeyEvent event) {
char c = event.getKeyChar();
int code = event.getKeyCode();
Sketch sketch = editor.getSketch();
JEditTextArea textarea = editor.getTextArea();
if ((event.getModifiers() & InputEvent.META_MASK) != 0) {
//event.consume(); // does nothing
return false;
}
if ((code == KeyEvent.VK_BACK_SPACE) || (code == KeyEvent.VK_TAB) ||
(code == KeyEvent.VK_ENTER) || ((c >= 32) && (c < 128))) {
sketch.setModified(true);
}
if ((code == KeyEvent.VK_UP) &&
((event.getModifiers() & InputEvent.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() & InputEvent.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() & InputEvent.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 ((event.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
textarea.setSelectionStart(caretIndex);
textarea.setSelectionEnd(index);
} else {
textarea.setCaretPosition(index);
}
event.consume();
// return true;
} else if (c == 9) {
if ((event.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
// if shift is down, the user always expects an outdent
// http://code.google.com/p/processing/issues/detail?id=458
editor.handleOutdent();
} else if (textarea.isSelectionActive()) {
editor.handleIndent();
} else if (Preferences.getBoolean("editor.tabs.expand")) {
int tabSize = Preferences.getInteger("editor.tabs.size");
textarea.setSelectedText(spaces(tabSize));
event.consume();
} else { // !Preferences.getBoolean("editor.tabs.expand")
textarea.setSelectedText("\t");
event.consume();
}
} else if (code == 10 || code == 13) { // auto-indent
if (Preferences.getBoolean("editor.indent")) {
char contents[] = textarea.getText().toCharArray();
int tabSize = Preferences.getInteger("editor.tabs.size");
// 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;
// if the previous thing is a brace (whether prev line or
// up farther) then the correct indent is the number of spaces
// on that line + 'indent'.
// if the previous line is not a brace, then just use the
// identical indentation to the previous line
// calculate the amount of indent on the previous line
// this will be used *only if the prev line is not an indent*
int spaceCount = calcSpaceCount(origIndex, contents);
// If the last character was a left curly brace, then indent.
// For 0122, walk backwards a bit to make sure that the there isn't a
// curly brace several spaces (or lines) back. Also moved this before
// calculating extraCount, since it'll affect that as well.
int index2 = origIndex;
while ((index2 >= 0) &&
Character.isWhitespace(contents[index2])) {
index2--;
}
if (index2 != -1) {
// still won't catch a case where prev stuff is a comment
if (contents[index2] == '{') {
// intermediate lines be damned,
// use the indent for this line instead
spaceCount = calcSpaceCount(index2, contents);
spaceCount += tabSize;
}
}
// now before inserting this many spaces, walk forward from
// the caret position and count the number of spaces,
// so that the number of spaces aren't duplicated again
int index = origIndex + 1;
int extraCount = 0;
while ((index < contents.length) &&
(contents[index] == ' ')) {
//spaceCount--;
extraCount++;
index++;
}
int braceCount = 0;
while ((index < contents.length) && (contents[index] != '\n')) {
if (contents[index] == '}') {
braceCount++;
}
index++;
}
// Hitting return on a line with spaces *after* the caret
// can cause trouble. For 0099, was ignoring the case, but this is
// annoying, so in 0122 we're trying to fix that.
spaceCount -= extraCount;
if (spaceCount < 0) {
// for rev 0122, actually delete extra space
//textarea.setSelectionStart(origIndex + 1);
textarea.setSelectionEnd(textarea.getSelectionStop() - spaceCount);
textarea.setSelectedText("\n");
textarea.setCaretPosition(textarea.getCaretPosition() + extraCount + spaceCount);
} else {
String insertion = "\n" + spaces(spaceCount);
textarea.setSelectedText(insertion);
textarea.setCaretPosition(textarea.getCaretPosition() + extraCount);
}
// not gonna bother handling more than one brace
if (braceCount > 0) {
int sel = textarea.getSelectionStart();
// sel - tabSize will be -1 if start/end parens on the same line
// http://dev.processing.org/bugs/show_bug.cgi?id=484
if (sel - tabSize >= 0) {
textarea.select(sel - tabSize, sel);
String s = spaces(tabSize);
// if these are spaces that we can delete
if (textarea.getSelectedText().equals(s)) {
textarea.setSelectedText("");
} else {
textarea.select(sel, sel);
}
}
}
} else {
// Enter/Return was being consumed by somehow even if false
// was returned, so this is a band-aid to simply fire the event again.
// http://dev.processing.org/bugs/show_bug.cgi?id=1073
textarea.setSelectedText(String.valueOf(c));
}
// mark this event as already handled (all but ignored)
event.consume();
// return true;
} else if (c == '}') {
if (Preferences.getBoolean("editor.indent")) {
// 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.getSelectionStop()) {
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;
int pairedSpaceCount = calcBraceIndent(prevCharIndex, contents); //, 1);
if (pairedSpaceCount == -1) return false;
textarea.setSelectionStart(lineStartIndex);
textarea.setSelectedText(spaces(pairedSpaceCount));
// mark this event as already handled
event.consume();
return true;
}
}
return false;
}
public boolean handleTyped(KeyEvent event) {
char c = event.getKeyChar();
if ((event.getModifiers() & InputEvent.CTRL_MASK) != 0) {
// on linux, ctrl-comma (prefs) being passed through to the editor
if (c == KeyEvent.VK_COMMA) {
event.consume();
return true;
}
// https://github.com/processing/processing/issues/3847
if (c == KeyEvent.VK_SPACE) {
event.consume();
return true;
}
}
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);
return calcSpaceCount(index, contents);
}
static private String spaces(int count) {
char[] c = new char[count];
Arrays.fill(c, ' ');
return new String(c);
}
}
@@ -0,0 +1,456 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2010-11 Ben Fry and Casey Reas
Copyright (c) 2012-15 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.swing.SwingUtilities;
import processing.app.*;
import processing.app.ui.Editor;
import processing.app.ui.EditorException;
import processing.app.ui.EditorState;
import processing.mode.java.runner.Runner;
import processing.mode.java.tweak.SketchParser;
public class JavaMode extends Mode {
public Editor createEditor(Base base, String path,
EditorState state) throws EditorException {
return new JavaEditor(base, path, state, this);
}
public JavaMode(Base base, File folder) {
super(base, folder);
// initLogger();
loadPreferences();
}
public String getTitle() {
return "Java";
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public File[] getExampleCategoryFolders() {
return new File[] {
new File(examplesFolder, "Basics"),
new File(examplesFolder, "Topics"),
new File(examplesFolder, "Demos"),
new File(examplesFolder, "Books")
};
}
public String getDefaultExtension() {
return "pde";
}
public String[] getExtensions() {
return new String[] { "pde", "java" };
}
public String[] getIgnorable() {
return new String[] {
"applet",
"application.macosx",
"application.windows",
"application.linux"
};
}
public Library getCoreLibrary() {
if (coreLibrary == null) {
File coreFolder = Platform.getContentFile("core");
coreLibrary = new Library(coreFolder);
// try {
// coreLibrary = getLibrary("processing.core");
// System.out.println("core found at " + coreLibrary.getLibraryPath());
// } catch (SketchException e) {
// Base.log("Serious problem while locating processing.core", e);
// }
}
return coreLibrary;
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/** Handles the standard Java "Run" or "Present" */
public Runner handleLaunch(Sketch sketch, RunnerListener listener,
final boolean present) throws SketchException {
JavaBuild build = new JavaBuild(sketch);
// String appletClassName = build.build(false);
String appletClassName = build.build(true);
if (appletClassName != null) {
final Runner runtime = new Runner(build, listener);
new Thread(new Runnable() {
public void run() {
// these block until finished
if (present) {
runtime.present(null);
} else {
runtime.launch(null);
}
}
}).start();
return runtime;
}
return null;
}
/** Start a sketch in tweak mode */
public Runner handleTweak(Sketch sketch,
RunnerListener listener) throws SketchException {
// final boolean present) throws SketchException {
final JavaEditor editor = (JavaEditor) listener;
// first try to build the unmodified code
JavaBuild build = new JavaBuild(sketch);
// String appletClassName = build.build(false);
String appletClassName = build.build(true);
if (appletClassName == null) {
// unmodified build failed, so fail
return null;
}
// if compilation passed, modify the code and build again
// save the original sketch code of the user
editor.initBaseCode();
// check for "// tweak" comment in the sketch
boolean requiresTweak = SketchParser.containsTweakComment(editor.baseCode);
// parse the saved sketch to get all (or only with "//tweak" comment) numbers
final SketchParser parser = new SketchParser(editor.baseCode, requiresTweak);
// add our code to the sketch
final boolean launchInteractive = editor.automateSketch(sketch, parser);
build = new JavaBuild(sketch);
appletClassName = build.build(false);
if (appletClassName != null) {
final Runner runtime = new Runner(build, listener);
new Thread(new Runnable() {
public void run() {
// these block until finished
// if (present) {
// runtime.present(null);
// } else {
runtime.launch(null);
// }
// next lines are executed when the sketch quits
if (launchInteractive) {
// fix swing deadlock issue: https://github.com/processing/processing/issues/3928
SwingUtilities.invokeLater(new Runnable() {
public void run() {
editor.initEditorCode(parser.allHandles, false);
editor.stopTweakMode(parser.allHandles);
}
});
}
}
}).start();
if (launchInteractive) {
// fix swing deadlock issue: https://github.com/processing/processing/issues/3928
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// replace editor code with baseCode
editor.initEditorCode(parser.allHandles, false);
editor.updateInterface(parser.allHandles, parser.colorBoxes);
editor.startTweakMode();
}
});
}
return runtime;
}
return null;
}
/*
// TODO Why is this necessary? Why isn't Sketch.isModified() used?
static private boolean isSketchModified(Sketch sketch) {
for (SketchCode sc : sketch.getCode()) {
if (sc.isModified()) {
return true;
}
}
return false;
}
*/
// public void handleStop() {
// if (runtime != null) {
// runtime.close(); // kills the window
// runtime = null; // will this help?
// }
// }
// public boolean handleExportApplet(Sketch sketch) throws SketchException, IOException {
// JavaBuild build = new JavaBuild(sketch);
// return build.exportApplet();
// }
public boolean handleExportApplication(Sketch sketch) throws SketchException, IOException {
JavaBuild build = new JavaBuild(sketch);
return build.exportApplication();
}
/**
* Any modes that extend JavaMode can override this method to add additional
* JARs to be included in the classpath for code completion and error checking
* @return searchPath: file-paths separated by File.pathSeparatorChar
*/
public String getSearchPath() {
return getCoreLibrary().getJarPath();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// Merged from ExperimentalMode
/*
void initLogger() {
final boolean VERBOSE_LOGGING = true;
final int LOG_SIZE = 512 * 1024; // max log file size (in bytes)
Logger globalLogger = Logger.getLogger("");
//Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); // doesn't work on os x
if (VERBOSE_LOGGING) {
globalLogger.setLevel(Level.INFO);
} else {
globalLogger.setLevel(Level.WARNING);
}
// enable logging to file
try {
// settings is writable for built-in modes, mode folder is not writable
File logFolder = Base.getSettingsFile("debug");
if (!logFolder.exists()) {
logFolder.mkdir();
}
File logFile = new File(logFolder, "DebugMode.%g.log");
Handler handler = new FileHandler(logFile.getAbsolutePath(), LOG_SIZE, 10, false);
globalLogger.addHandler(handler);
} catch (IOException ex) {
Logger.getLogger(JavaMode.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(JavaMode.class.getName()).log(Level.SEVERE, null, ex);
}
}
*/
//ImageIcon classIcon, fieldIcon, methodIcon, localVarIcon;
// protected void loadIcons() {
// String iconPath = getContentFile("data").getAbsolutePath() + File.separator + "icons";
// classIcon = new ImageIcon(iconPath + File.separator + "class_obj.png");
// methodIcon = new ImageIcon(iconPath + File.separator + "methpub_obj.png");
// fieldIcon = new ImageIcon(iconPath + File.separator + "field_protected_obj.png");
// localVarIcon = new ImageIcon(iconPath + File.separator + "field_default_obj.png");
// }
static public volatile boolean errorCheckEnabled = true;
static public volatile boolean warningsEnabled = true;
static public volatile boolean codeCompletionsEnabled = true;
static public volatile boolean debugOutputEnabled = false;
static public volatile boolean errorLogsEnabled = false;
static public volatile boolean autoSaveEnabled = true;
static public volatile boolean autoSavePromptEnabled = true;
static public volatile boolean defaultAutoSaveEnabled = true;
static public volatile boolean ccTriggerEnabled = false;
static public volatile boolean importSuggestEnabled = true;
static public volatile boolean inspectModeHotkeyEnabled = true;
static public int autoSaveInterval = 3; //in minutes
/**
* After how many typed characters, code completion is triggered
*/
volatile public static int codeCompletionTriggerLength = 1;
static public final String prefErrorCheck = "pdex.errorCheckEnabled";
static public final String prefWarnings = "pdex.warningsEnabled";
static public final String prefDebugOP = "pdex.dbgOutput";
static public final String prefErrorLogs = "pdex.writeErrorLogs";
static public final String prefAutoSaveInterval = "pdex.autoSaveInterval";
static public final String prefAutoSave = "pdex.autoSave.autoSaveEnabled";
static public final String prefAutoSavePrompt = "pdex.autoSave.promptDisplay";
static public final String prefDefaultAutoSave = "pdex.autoSave.autoSaveByDefault";
static public final String suggestionsFileName = "suggestions.txt";
static public final String COMPLETION_PREF = "pdex.completion";
static public final String COMPLETION_TRIGGER_PREF = "pdex.completion.trigger";
static public final String SUGGEST_IMPORTS_PREF = "pdex.suggest.imports";
static public final String INSPECT_MODE_HOTKEY_PREF = "pdex.inspectMode.hotkey";
// static volatile public boolean enableTweak = false;
/**
* Stores the white list/black list of allowed/blacklisted imports. These are defined in
* suggestions.txt in java mode folder.
*/
static public final Map<String, Set<String>> suggestionsMap = new HashMap<>();
public void loadPreferences() {
Messages.log("Load PDEX prefs");
ensurePrefsExist();
errorCheckEnabled = Preferences.getBoolean(prefErrorCheck);
warningsEnabled = Preferences.getBoolean(prefWarnings);
codeCompletionsEnabled = Preferences.getBoolean(COMPLETION_PREF);
// DEBUG = Preferences.getBoolean(prefDebugOP);
errorLogsEnabled = Preferences.getBoolean(prefErrorLogs);
autoSaveInterval = Preferences.getInteger(prefAutoSaveInterval);
// untitledAutoSaveEnabled = Preferences.getBoolean(prefUntitledAutoSave);
autoSaveEnabled = Preferences.getBoolean(prefAutoSave);
autoSavePromptEnabled = Preferences.getBoolean(prefAutoSavePrompt);
defaultAutoSaveEnabled = Preferences.getBoolean(prefDefaultAutoSave);
ccTriggerEnabled = Preferences.getBoolean(COMPLETION_TRIGGER_PREF);
importSuggestEnabled = Preferences.getBoolean(SUGGEST_IMPORTS_PREF);
inspectModeHotkeyEnabled = Preferences.getBoolean(INSPECT_MODE_HOTKEY_PREF);
loadSuggestionsMap();
}
public void savePreferences() {
Messages.log("Saving PDEX prefs");
Preferences.setBoolean(prefErrorCheck, errorCheckEnabled);
Preferences.setBoolean(prefWarnings, warningsEnabled);
Preferences.setBoolean(COMPLETION_PREF, codeCompletionsEnabled);
// Preferences.setBoolean(prefDebugOP, DEBUG);
Preferences.setBoolean(prefErrorLogs, errorLogsEnabled);
Preferences.setInteger(prefAutoSaveInterval, autoSaveInterval);
// Preferences.setBoolean(prefUntitledAutoSave,untitledAutoSaveEnabled);
Preferences.setBoolean(prefAutoSave, autoSaveEnabled);
Preferences.setBoolean(prefAutoSavePrompt, autoSavePromptEnabled);
Preferences.setBoolean(prefDefaultAutoSave, defaultAutoSaveEnabled);
Preferences.setBoolean(COMPLETION_TRIGGER_PREF, ccTriggerEnabled);
Preferences.setBoolean(SUGGEST_IMPORTS_PREF, importSuggestEnabled);
Preferences.setBoolean(INSPECT_MODE_HOTKEY_PREF, inspectModeHotkeyEnabled);
}
public void loadSuggestionsMap() {
File suggestionsListFile = new File(getFolder() + File.separator
+ suggestionsFileName);
if (!suggestionsListFile.exists()) {
Messages.loge("Suggestions file not found! "
+ suggestionsListFile.getAbsolutePath());
return;
}
try {
BufferedReader br = new BufferedReader(
new FileReader(suggestionsListFile));
while (true) {
String line = br.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.startsWith("#")) {
continue;
} else {
if (line.contains("=")) {
String key = line.split("=")[0];
String val = line.split("=")[1];
if (suggestionsMap.containsKey(key)) {
suggestionsMap.get(key).add(val);
} else {
HashSet<String> set = new HashSet<>();
set.add(val);
suggestionsMap.put(key, set);
}
}
}
}
br.close();
} catch (IOException e) {
Messages.loge("IOException while reading suggestions file:"
+ suggestionsListFile.getAbsolutePath());
}
}
public void ensurePrefsExist() {
//TODO: Need to do a better job of managing prefs. Think lists.
if (Preferences.get(prefErrorCheck) == null)
Preferences.setBoolean(prefErrorCheck, errorCheckEnabled);
if (Preferences.get(prefWarnings) == null)
Preferences.setBoolean(prefWarnings, warningsEnabled);
if (Preferences.get(COMPLETION_PREF) == null)
Preferences.setBoolean(COMPLETION_PREF, codeCompletionsEnabled);
if (Preferences.get(prefDebugOP) == null)
// Preferences.setBoolean(prefDebugOP, DEBUG);
if (Preferences.get(prefErrorLogs) == null)
Preferences.setBoolean(prefErrorLogs, errorLogsEnabled);
if (Preferences.get(prefAutoSaveInterval) == null)
Preferences.setInteger(prefAutoSaveInterval, autoSaveInterval);
// if(Preferences.get(prefUntitledAutoSave) == null)
// Preferences.setBoolean(prefUntitledAutoSave,untitledAutoSaveEnabled);
if (Preferences.get(prefAutoSave) == null)
Preferences.setBoolean(prefAutoSave, autoSaveEnabled);
if (Preferences.get(prefAutoSavePrompt) == null)
Preferences.setBoolean(prefAutoSavePrompt, autoSavePromptEnabled);
if (Preferences.get(prefDefaultAutoSave) == null)
Preferences.setBoolean(prefDefaultAutoSave, defaultAutoSaveEnabled);
if (Preferences.get(COMPLETION_TRIGGER_PREF) == null)
Preferences.setBoolean(COMPLETION_TRIGGER_PREF, ccTriggerEnabled);
if (Preferences.get(SUGGEST_IMPORTS_PREF) == null)
Preferences.setBoolean(SUGGEST_IMPORTS_PREF, importSuggestEnabled);
if (Preferences.get(INSPECT_MODE_HOTKEY_PREF) == null)
Preferences.setBoolean(INSPECT_MODE_HOTKEY_PREF, inspectModeHotkeyEnabled);
}
static public void main(String[] args) {
processing.app.Base.main(args);
}
}
@@ -0,0 +1,171 @@
/*
Part of the Processing project - http://processing.org
Copyright (c) 2013-15 The Processing Foundation
Copyright (c) 2010-13 Ben Fry and Casey Reas
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Box;
import javax.swing.JLabel;
import processing.app.Language;
import processing.app.ui.Editor;
import processing.app.ui.EditorButton;
import processing.app.ui.EditorToolbar;
public class JavaToolbar extends EditorToolbar {
JavaEditor jeditor;
// boolean debug; // true if this is the expanded debug feller
EditorButton stepButton;
EditorButton continueButton;
// public JavaToolbar(Editor editor, boolean debug) {
public JavaToolbar(Editor editor) {
super(editor);
// this.debug = debug;
jeditor = (JavaEditor) editor;
}
@Override
public List<EditorButton> createButtons() {
// jeditor not ready yet because this is called by super()
final boolean debug = ((JavaEditor) editor).isDebuggerEnabled();
// System.out.println("creating buttons in JavaToolbar, debug:" + debug);
List<EditorButton> outgoing = new ArrayList<>();
final String runText = debug ?
Language.text("toolbar.debug") : Language.text("toolbar.run");
runButton = new EditorButton(this,
"/lib/toolbar/run",
runText,
Language.text("toolbar.present")) {
@Override
public void actionPerformed(ActionEvent e) {
handleRun(e.getModifiers());
}
};
outgoing.add(runButton);
if (debug) {
stepButton = new EditorButton(this,
"/lib/toolbar/step",
Language.text("menu.debug.step"),
Language.text("menu.debug.step_into"),
Language.text("menu.debug.step_out")) {
@Override
public void actionPerformed(ActionEvent e) {
final int mask = ActionEvent.SHIFT_MASK | ActionEvent.ALT_MASK;
jeditor.handleStep(e.getModifiers() & mask);
}
};
outgoing.add(stepButton);
continueButton = new EditorButton(this,
"/lib/toolbar/continue",
Language.text("menu.debug.continue")) {
@Override
public void actionPerformed(ActionEvent e) {
jeditor.handleContinue();
}
};
outgoing.add(continueButton);
}
stopButton = new EditorButton(this,
"/lib/toolbar/stop",
Language.text("toolbar.stop")) {
@Override
public void actionPerformed(ActionEvent e) {
handleStop();
}
};
outgoing.add(stopButton);
return outgoing;
}
@Override
public void addModeButtons(Box box, JLabel label) {
EditorButton debugButton =
new EditorButton(this, "/lib/toolbar/debug",
Language.text("toolbar.debug")) {
@Override
public void actionPerformed(ActionEvent e) {
jeditor.toggleDebug();
}
};
if (((JavaEditor) editor).isDebuggerEnabled()) {
debugButton.setSelected(true);
}
// debugButton.setRolloverLabel(label);
box.add(debugButton);
addGap(box);
}
@Override
public void handleRun(int modifiers) {
boolean shift = (modifiers & InputEvent.SHIFT_MASK) != 0;
if (shift) {
jeditor.handlePresent();
} else {
jeditor.handleRun();
}
}
@Override
public void handleStop() {
jeditor.handleStop();
}
public void activateContinue() {
continueButton.setSelected(true);
repaint();
}
protected void deactivateContinue() {
continueButton.setSelected(false);
repaint();
}
protected void activateStep() {
stepButton.setSelected(true);
repaint();
}
protected void deactivateStep() {
stepButton.setSelected(false);
repaint();
}
}
@@ -0,0 +1,60 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import com.sun.jdi.ArrayReference;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.Value;
import processing.app.Messages;
/**
* Specialized {@link VariableNode} for representing single fields in an array.
* Overrides {@link #setValue} to properly change the value of the encapsulated
* array field.
*/
public class ArrayFieldNode extends VariableNode {
protected ArrayReference array;
protected int index;
/**
* Construct an {@link ArrayFieldNode}.
*/
public ArrayFieldNode(String name, String type, Value value, ArrayReference array, int index) {
super(name, type, value);
this.array = array;
this.index = index;
}
@Override
public void setValue(Value value) {
try {
array.setValue(index, value);
} catch (InvalidTypeException | ClassNotLoadedException ex) {
Messages.loge(null, ex);
}
this.value = value;
}
}
@@ -0,0 +1,38 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import com.sun.jdi.ReferenceType;
/**
* Listener to be notified when a class is loaded in the debugger. Used by
* {@link LineBreakpoint}s to activate themselves as soon as the respective
* class is loaded.
*/
public interface ClassLoadListener {
/**
* Event handler called when a class is loaded.
*/
public void classLoaded(ReferenceType theClass);
}
@@ -0,0 +1,65 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.Field;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.Value;
import processing.app.Messages;
/**
* Specialized {@link VariableNode} for representing fields. Overrides
* {@link #setValue} to properly change the value of the encapsulated field.
*/
public class FieldNode extends VariableNode {
protected Field field;
protected ObjectReference obj;
/**
* Construct a {@link FieldNode}.
* @param obj a reference to the object containing the field
*/
public FieldNode(String name, String type, Value value, Field field,
ObjectReference obj) {
super(name, type, value);
this.field = field;
this.obj = obj;
}
@Override
public void setValue(Value value) {
try {
obj.setValue(field, value);
} catch (InvalidTypeException ite) {
Messages.loge(null, ite);
} catch (ClassNotLoadedException cnle) {
Messages.loge(null, cnle);
}
this.value = value;
}
}
@@ -0,0 +1,255 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import java.util.List;
import processing.app.Messages;
import processing.mode.java.Debugger;
import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.request.BreakpointRequest;
/**
* Model/Controller of a line breakpoint. Can be set before or while debugging.
* Adds a highlight using the debuggers view ({@link DebugEditor}).
*/
public class LineBreakpoint implements ClassLoadListener {
protected Debugger dbg; // the debugger
protected LineID line; // the line this breakpoint is set on
protected BreakpointRequest bpr; // the request on the VM's event request manager
protected String className;
/**
* Create a {@link LineBreakpoint}. If in a debug session, will try to
* immediately set the breakpoint. If not in a debug session or the
* corresponding class is not yet loaded the breakpoint will activate on
* class load.
*
* @param line the line id to create the breakpoint on
* @param dbg the {@link Debugger}
*/
public LineBreakpoint(LineID line, Debugger dbg) {
this.line = line;
line.startTracking(dbg.getEditor().getTab(line.fileName()).getDocument());
this.dbg = dbg;
this.className = className();
set(); // activate the breakpoint (show highlight, attach if debugger is running)
Messages.log("LBP Created " + toString() + " class: " + this.className);
}
/**
* Create a {@link LineBreakpoint} on a line in the current tab.
* @param lineIdx the line index of the current tab to create the breakpoint
*/
// TODO: remove and replace by {@link #LineBreakpoint(LineID line, Debugger dbg)}
public LineBreakpoint(int lineIdx, Debugger dbg) {
this(dbg.getEditor().getLineIDInCurrentTab(lineIdx), dbg);
}
/**
* Get the line id this breakpoint is on.
*/
public LineID lineID() {
return line;
}
/**
* Test if this breakpoint is on a certain line.
*
* @param testLine the line id to test
* @return true if this breakpoint is on the given line
*/
public boolean isOnLine(LineID testLine) {
return line.equals(testLine);
}
/**
* Attach this breakpoint to the VM. Creates and enables a
* {@link BreakpointRequest}. VM needs to be paused.
*
* @param theClass class to attach to
* @return true on success
*/
protected boolean attach(ReferenceType theClass) {
if (theClass == null || className == null ||
!className.equals(parseTopLevelClassName(theClass.name()))) {
return false;
}
log("trying to attach: " + line.fileName + ":" + line.lineIdx + " to " + theClass.name());
if (!dbg.isPaused()) {
log("can't attach breakpoint, debugger not paused");
return false;
}
// find line in java space
LineID javaLine = dbg.sketchToJavaLine(line);
if (javaLine == null) {
log("couldn't find line " + line + " in the java code");
return false;
}
try {
log("BPs of class: " + theClass + ", line " + (javaLine.lineIdx() + 1));
List<Location> locations = theClass.locationsOfLine(javaLine.lineIdx() + 1);
if (locations.isEmpty()) {
log("no location found for line " + line + " -> " + javaLine);
return false;
}
// use first found location
bpr = dbg.vm().eventRequestManager().createBreakpointRequest(locations.get(0));
bpr.enable();
log("attached breakpoint to " + line + " -> " + javaLine);
return true;
} catch (AbsentInformationException ex) {
Messages.loge(null, ex);
}
return false;
}
protected boolean isAttached() {
return bpr != null;
}
/**
* Detach this breakpoint from the VM. Deletes the
* {@link BreakpointRequest}.
*/
public void detach() {
if (bpr != null) {
try {
dbg.vm().eventRequestManager().deleteEventRequest(bpr);
} catch (VMDisconnectedException ignore) { }
bpr = null;
}
}
/**
* Set this breakpoint. Adds the line highlight. If Debugger is paused
* also attaches the breakpoint by calling {@link #attach()}.
*/
protected void set() {
dbg.addClassLoadListener(this); // class may not yet be loaded
dbg.getEditor().addBreakpointedLine(line);
if (className != null && dbg.isPaused()) { // debugging right now, try to attach
for (ReferenceType rt : dbg.getClasses()) {
// try to attach to all top level or nested classes
if (attach(rt)) break;
}
}
if (dbg.getEditor().isInCurrentTab(line)) {
dbg.getEditor().getSketch().setModified(true);
}
}
/**
* Remove this breakpoint. Clears the highlight and detaches
* the breakpoint if the debugger is paused.
*/
public void remove() {
dbg.removeClassLoadListener(this);
//System.out.println("removing " + line.lineIdx());
dbg.getEditor().removeBreakpointedLine(line.lineIdx());
if (dbg.isPaused()) {
// immediately remove the breakpoint
detach();
}
line.stopTracking();
if (dbg.getEditor().isInCurrentTab(line)) {
dbg.getEditor().getSketch().setModified(true);
}
}
@Override
public String toString() {
return line.toString();
}
/**
* Get the name of the class this breakpoint belongs to. Needed for
* fetching the right location to create a breakpoint request.
* @return the class name
*/
protected String className() {
if (line.fileName().endsWith(".pde")) {
// standard tab
return dbg.getEditor().getSketch().getName();
}
if (line.fileName().endsWith(".java")) {
// pure java tab
return line.fileName().substring(0, line.fileName().lastIndexOf(".java"));
}
return null;
}
/**
* Event handler called when a class is loaded in the debugger. Causes the
* breakpoint to be attached, if its class was loaded.
*
* @param theClass the class that was just loaded.
*/
@Override
public void classLoaded(ReferenceType theClass) {
if (!isAttached()) {
// try to attach
attach(theClass);
}
}
static public String parseTopLevelClassName(String name) {
// Get rid of nested class name
int dollar = name.indexOf('$');
return (dollar == -1) ? name : name.substring(0, dollar);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private void log(String msg, Object... args) {
if (args != null && args.length != 0) {
Messages.logf(getClass().getName() + " " + msg, args);
} else {
Messages.log(getClass().getName() + " " + msg);
}
}
}
@@ -0,0 +1,175 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import java.util.HashSet;
import java.util.Set;
import processing.mode.java.JavaEditor;
/**
* Model/Controller for a highlighted source code line. Implements a custom
* background color and a text based marker placed in the left-hand gutter area.
*/
public class LineHighlight {
protected final JavaEditor editor; // the view, used for highlighting lines by setting a background color
protected final LineID lineID; // the id of the line
protected String marker; //
protected int priority = 0;
protected static final Set<LineHighlight> allHighlights = new HashSet<>();
/**
* Create a {@link LineHighlight}.
*/
public LineHighlight(LineID lineID, JavaEditor editor) {
this.lineID = lineID;
this.editor = editor;
lineID.addListener(this);
lineID.startTracking(editor.getTab(lineID.fileName()).getDocument()); // TODO: overwrite a previous doc?
paint(); // already checks if on current tab
allHighlights.add(this);
}
protected static boolean isHighestPriority(LineHighlight hl) {
for (LineHighlight check : allHighlights) {
if (check.getLineID().equals(hl.getLineID()) &&
check.priority() > hl.priority()) {
return false;
}
}
return true;
}
public void setPriority(int p) {
this.priority = p;
}
public int priority() {
return priority;
}
/**
* Create a {@link LineHighlight} on the current tab.
*
* @param lineIdx the line index on the current tab to highlight
* @param editor the {@link JavaEditor}
*/
// TODO: Remove and replace by {@link #LineHighlight(LineID lineID, JavaEditor editor)}
public LineHighlight(int lineIdx, JavaEditor editor) {
this(editor.getLineIDInCurrentTab(lineIdx), editor);
}
/**
* Set a text based marker displayed in the left hand gutter area of this
* highlighted line.
*
* @param marker the marker text
*/
public void setMarker(String marker) {
this.marker = marker;
paint();
}
/**
* Retrieve the line id of this {@link LineHighlight}.
*
* @return the line id
*/
public LineID getLineID() {
return lineID;
}
/**
* Test if this highlight is on a certain line.
*
* @param testLine the line to test
* @return true if this highlight is on the given line
*/
public boolean isOnLine(LineID testLine) {
return lineID.equals(testLine);
}
/**
* Event handler for line number changes (due to editing). Will remove the
* highlight from the old line number and repaint it at the new location.
*
* @param line the line that has changed
* @param oldLineIdx the old line index (0-based)
* @param newLineIdx the new line index (0-based)
*/
public void lineChanged(LineID line, int oldLineIdx, int newLineIdx) {
// clear old line
if (editor.isInCurrentTab(new LineID(line.fileName(), oldLineIdx))) {
editor.getJavaTextArea().clearGutterText(oldLineIdx);
}
// paint new line
// but only if it's on top -> fixes current line being hidden by breakpoint moving it down.
// lineChanged events seem to come in inverse order of startTracking the LineID. (and bp is created first...)
if (LineHighlight.isHighestPriority(this)) {
paint();
}
}
/**
* Notify this line highlight that it is no longer used. Call this for
* cleanup before the {@link LineHighlight} is discarded.
*/
public void dispose() {
lineID.removeListener(this);
lineID.stopTracking();
allHighlights.remove(this);
}
/**
* (Re-)paint this line highlight.
*/
public void paint() {
if (editor.isInCurrentTab(lineID)) {
if (marker != null) {
editor.getJavaTextArea().setGutterText(lineID.lineIdx(), marker);
}
}
}
/**
* Clear this line highlight.
*/
public void clear() {
if (editor.isInCurrentTab(lineID)) {
editor.getJavaTextArea().clearGutterText(lineID.lineIdx());
}
}
}
@@ -0,0 +1,277 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import java.util.HashSet;
import java.util.Set;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Position;
import processing.app.Messages;
/**
* Describes an ID for a code line. Comprised of a file name and a (0-based)
* line number. Can track changes to the line number due to text editing by
* attaching a {@link Document}. Registered {@link LineListener}s are notified
* of changes to the line number.
*/
public class LineID implements DocumentListener {
protected String fileName; // the filename
protected int lineIdx; // the line number, 0-based
protected Document doc; // the Document to use for line number tracking
protected Position pos; // the Position acquired during line number tracking
protected Set<LineHighlight> listeners = new HashSet<LineHighlight>(); // listeners for line number changes
public LineID(String fileName, int lineIdx) {
this.fileName = fileName;
this.lineIdx = lineIdx;
}
/**
* Get the file name of this line.
*
* @return the file name
*/
public String fileName() {
return fileName;
}
/**
* Get the (0-based) line number of this line.
*
* @return the line index (i.e. line number, starting at 0)
*/
public synchronized int lineIdx() {
return lineIdx;
}
@Override
public int hashCode() {
return toString().hashCode();
}
/**
* Test whether this {@link LineID} is equal to another object. Two
* {@link LineID}'s are equal when both their fileName and lineNo are equal.
*
* @param obj the object to test for equality
* @return {@code true} if equal
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final LineID other = (LineID) obj;
if ((this.fileName == null) ? (other.fileName != null) : !this.fileName.equals(other.fileName)) {
return false;
}
if (this.lineIdx != other.lineIdx) {
return false;
}
return true;
}
/**
* Output a string representation in the form fileName:lineIdx+1. Note this
* uses a 1-based line number as is customary for human-readable line
* numbers.
*
* @return the string representation of this line ID
*/
@Override
public String toString() {
return fileName + ":" + (lineIdx + 1);
}
/**
* Attach a {@link Document} to enable line number tracking when editing.
* The position to track is before the first non-whitespace character on the
* line. Edits happening before that position will cause the line number to
* update accordingly. Multiple {@link #startTracking} calls will replace
* the tracked document. Whoever wants a tracked line should track it and
* add itself as listener if necessary.
* ({@link LineHighlight}, {@link LineBreakpoint})
*
* @param doc the {@link Document} to use for line number tracking
*/
public synchronized void startTracking(Document doc) {
//System.out.println("tracking: " + this);
if (doc == null) {
return; // null arg
}
if (doc == this.doc) {
return; // already tracking that doc
}
try {
Element line = doc.getDefaultRootElement().getElement(lineIdx);
if (line == null) {
return; // line doesn't exist
}
String lineText = doc.getText(line.getStartOffset(), line.getEndOffset() - line.getStartOffset());
// set tracking position at (=before) first non-white space character on line,
// or, if the line consists of entirely white spaces, just before the newline
// character
pos = doc.createPosition(line.getStartOffset() + nonWhiteSpaceOffset(lineText));
this.doc = doc;
doc.addDocumentListener(this);
} catch (BadLocationException ex) {
Messages.loge(null, ex);
pos = null;
this.doc = null;
}
}
/**
* Notify this {@link LineID} that it is no longer in use. Will stop
* position tracking. Call this when this {@link LineID} is no longer
* needed.
*/
public synchronized void stopTracking() {
if (doc != null) {
doc.removeDocumentListener(this);
doc = null;
}
}
/**
* Update the tracked position. Will notify listeners if line number has
* changed.
*/
protected synchronized void updatePosition() {
if (doc != null && pos != null) {
// track position
int offset = pos.getOffset();
int oldLineIdx = lineIdx;
lineIdx = doc.getDefaultRootElement().getElementIndex(offset); // offset to lineNo
if (lineIdx != oldLineIdx) {
for (LineHighlight l : listeners) {
if (l != null) {
l.lineChanged(this, oldLineIdx, lineIdx);
} else {
listeners.remove(l); // remove null listener
}
}
}
}
}
/**
* Add listener to be notified when the line number changes.
*
* @param l the listener to add
*/
public void addListener(LineHighlight l) {
listeners.add(l);
}
/**
* Remove a listener for line number changes.
*
* @param l the listener to remove
*/
public void removeListener(LineHighlight l) {
listeners.remove(l);
}
/**
* Calculate the offset of the first non-whitespace character in a string.
* @param str the string to examine
* @return offset of first non-whitespace character in str
*/
protected static int nonWhiteSpaceOffset(String str) {
for (int i = 0; i < str.length(); i++) {
if (!Character.isWhitespace(str.charAt(i))) {
return i;
}
}
// If we've reached here, that implies the line consists of purely white
// space. So return at a position just after the whitespace (i.e.,
// just before the newline).
//
// The " - 1" part resolves issue #3552
return str.length() - 1;
}
/**
* Called when the {@link Document} registered using {@link #startTracking}
* is edited. This happens when text is inserted or removed.
*/
protected void editEvent(DocumentEvent de) {
//System.out.println("document edit @ " + de.getOffset());
if (de.getOffset() <= pos.getOffset()) {
updatePosition();
//System.out.println("updating, new line no: " + lineNo);
}
}
/**
* {@link DocumentListener} callback. Called when text is inserted.
*/
@Override
public void insertUpdate(DocumentEvent de) {
editEvent(de);
}
/**
* {@link DocumentListener} callback. Called when text is removed.
*/
@Override
public void removeUpdate(DocumentEvent de) {
editEvent(de);
}
/**
* {@link DocumentListener} callback. Called when attributes are changed.
* Not used.
*/
@Override
public void changedUpdate(DocumentEvent de) {
// not needed.
}
}
@@ -0,0 +1,59 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.StackFrame;
import com.sun.jdi.Value;
import processing.app.Messages;
/**
* Specialized {@link VariableNode} for representing local variables.
* Overrides {@link #setValue} to properly change the value of the
* encapsulated local variable.
*/
public class LocalVariableNode extends VariableNode {
protected LocalVariable var;
protected StackFrame frame;
public LocalVariableNode(String name, String type, Value value, LocalVariable var, StackFrame frame) {
super(name, type, value);
this.var = var;
this.frame = frame;
}
@Override
public void setValue(Value value) {
try {
frame.setValue(var, value);
} catch (InvalidTypeException | ClassNotLoadedException ex) {
Messages.loge(null, ex);
}
this.value = value;
}
}
@@ -0,0 +1,383 @@
/* -*- 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.debug;
import com.sun.jdi.ArrayReference;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.StringReference;
import com.sun.jdi.Value;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
/**
* Model for a variable in the variable inspector. Has a type and name and
* optionally a value. Can have sub-variables (as is the case for objects,
* and arrays).
*/
public class VariableNode implements MutableTreeNode {
public static final int TYPE_UNKNOWN = -1;
public static final int TYPE_OBJECT = 0;
public static final int TYPE_ARRAY = 1;
public static final int TYPE_INTEGER = 2;
public static final int TYPE_FLOAT = 3;
public static final int TYPE_BOOLEAN = 4;
public static final int TYPE_CHAR = 5;
public static final int TYPE_STRING = 6;
public static final int TYPE_LONG = 7;
public static final int TYPE_DOUBLE = 8;
public static final int TYPE_BYTE = 9;
public static final int TYPE_SHORT = 10;
public static final int TYPE_VOID = 11;
protected String type;
protected String name;
protected Value value;
protected List<MutableTreeNode> children = new ArrayList<>();
protected MutableTreeNode parent;
/**
* Construct a {@link VariableNode}.
* @param name the name
* @param type the type
* @param value the value
*/
public VariableNode(String name, String type, Value value) {
this.name = name;
this.type = type;
this.value = value;
}
public void setValue(Value value) {
this.value = value;
}
public Value getValue() {
return value;
}
/**
* Get a String representation of this variable nodes value.
*
* @return a String representing the value.
*/
public String getStringValue() {
String str;
if (value != null) {
if (getType() == TYPE_OBJECT) {
str = "instance of " + type;
} else if (getType() == TYPE_ARRAY) {
//instance of int[5] (id=998) --> instance of int[5]
str = value.toString().substring(0, value.toString().lastIndexOf(" "));
} else if (getType() == TYPE_STRING) {
str = ((StringReference) value).value(); // use original string value (without quotes)
} else {
str = value.toString();
}
} else {
str = "null";
}
return str;
}
public String getTypeName() {
return type;
}
public int getType() {
if (type == null) {
return TYPE_UNKNOWN;
}
if (type.endsWith("[]")) {
return TYPE_ARRAY;
}
if (type.equals("int")) {
return TYPE_INTEGER;
}
if (type.equals("long")) {
return TYPE_LONG;
}
if (type.equals("byte")) {
return TYPE_BYTE;
}
if (type.equals("short")) {
return TYPE_SHORT;
}
if (type.equals("float")) {
return TYPE_FLOAT;
}
if (type.equals("double")) {
return TYPE_DOUBLE;
}
if (type.equals("char")) {
return TYPE_CHAR;
}
if (type.equals("java.lang.String")) {
return TYPE_STRING;
}
if (type.equals("boolean")) {
return TYPE_BOOLEAN;
}
if (type.equals("void")) {
return TYPE_VOID; //TODO: check if this is correct
}
return TYPE_OBJECT;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* Add a {@link VariableNode} as child.
*
* @param c the {@link VariableNode} to add.
*/
public void addChild(VariableNode c) {
children.add(c);
c.setParent(this);
}
/**
* Add multiple {@link VariableNode}s as children.
*
* @param children the list of {@link VariableNode}s to add.
*/
public void addChildren(List<VariableNode> children) {
for (VariableNode child : children) {
addChild(child);
}
}
@Override
public TreeNode getChildAt(int i) {
return children.get(i);
}
@Override
public int getChildCount() {
return children.size();
}
@Override
public TreeNode getParent() {
return parent;
}
@Override
public int getIndex(TreeNode tn) {
return children.indexOf(tn);
}
@Override
public boolean getAllowsChildren() {
if (value == null) {
return false;
}
// handle strings
if (getType() == TYPE_STRING) {
return false;
}
// handle arrays
if (getType() == TYPE_ARRAY) {
ArrayReference array = (ArrayReference) value;
return array.length() > 0;
}
// handle objects
if (getType() == TYPE_OBJECT) { // this also rules out null
// check if this object has any fields
ObjectReference obj = (ObjectReference) value;
return !obj.referenceType().visibleFields().isEmpty();
}
return false;
}
/**
* This controls the default icon and disclosure triangle.
*
* @return true, will show "folder" icon and disclosure triangle.
*/
@Override
public boolean isLeaf() {
//return children.size() == 0;
return !getAllowsChildren();
}
@Override
public Enumeration<MutableTreeNode> children() {
return Collections.enumeration(children);
}
/**
* Get a String representation of this {@link VariableNode}.
*
* @return the name of the variable (for sorting to work).
*/
@Override
public String toString() {
return getName(); // for sorting
}
/**
* Get a String description of this {@link VariableNode}. Contains the type,
* name and value.
*
* @return the description
*/
public String getDescription() {
String str = "";
if (type != null) {
str += type + " ";
}
str += name;
str += " = " + getStringValue();
return str;
}
@Override
public void insert(MutableTreeNode mtn, int i) {
children.add(i, this);
}
@Override
public void remove(int i) {
MutableTreeNode mtn = children.remove(i);
if (mtn != null) {
mtn.setParent(null);
}
}
@Override
public void remove(MutableTreeNode mtn) {
children.remove(mtn);
mtn.setParent(null);
}
/**
* Remove all children from this {@link VariableNode}.
*/
public void removeAllChildren() {
for (MutableTreeNode mtn : children) {
mtn.setParent(null);
}
children.clear();
}
@Override
public void setUserObject(Object o) {
if (o instanceof Value) {
value = (Value) o;
}
}
@Override
public void removeFromParent() {
parent.remove(this);
this.parent = null;
}
@Override
public void setParent(MutableTreeNode mtn) {
parent = mtn;
}
/**
* Test for equality. To be equal, two {@link VariableNode}s need to have
* equal type, name and value.
*
* @param obj the object to test for equality with this {@link VariableNode}
* @return true if the given object is equal to this {@link VariableNode}
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final VariableNode other = (VariableNode) obj;
if ((this.type == null) ? (other.type != null) : !this.type.equals(other.type)) {
//System.out.println("type not equal");
return false;
}
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
//System.out.println("name not equal");
return false;
}
if (this.value != other.value && (this.value == null || !this.value.equals(other.value))) {
//System.out.println("value not equal");
return false;
}
return true;
}
/**
* Returns a hash code based on type, name and value.
*/
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + (this.type != null ? this.type.hashCode() : 0);
hash = 97 * hash + (this.name != null ? this.name.hashCode() : 0);
hash = 97 * hash + (this.value != null ? this.value.hashCode() : 0);
return hash;
}
}
@@ -0,0 +1,183 @@
package processing.mode.java.pdex;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import processing.app.Messages;
public class ASTUtils {
public static ASTNode getASTNodeAt(ASTNode root, int startJavaOffset, int stopJavaOffset) {
Messages.log("* getASTNodeAt");
int length = stopJavaOffset - startJavaOffset;
NodeFinder f = new NodeFinder(root, startJavaOffset, length);
ASTNode node = f.getCoveredNode();
if (node == null) {
node = f.getCoveringNode();
}
if (node == null) {
Messages.log("no node found");
} else {
Messages.log("found " + node.getClass().getSimpleName());
}
return node;
}
public static SimpleName getSimpleNameAt(ASTNode root, int startJavaOffset, int stopJavaOffset) {
Messages.log("* getSimpleNameAt");
// Find node at offset
ASTNode node = getASTNodeAt(root, startJavaOffset, stopJavaOffset);
SimpleName result = null;
if (node == null) {
result = null;
} else if (node.getNodeType() == ASTNode.SIMPLE_NAME) {
result = (SimpleName) node;
} else {
// Return SimpleName with highest coverage
List<SimpleName> simpleNames = getSimpleNameChildren(node);
if (!simpleNames.isEmpty()) {
// Compute coverage <selection x node>
int[] coverages = simpleNames.stream()
.mapToInt(name -> {
int start = name.getStartPosition();
int stop = start + name.getLength();
return Math.min(stop, stopJavaOffset) -
Math.max(startJavaOffset, start);
})
.toArray();
// Select node with highest coverage
int maxIndex = IntStream.range(0, simpleNames.size())
.filter(i -> coverages[i] >= 0)
.reduce((i, j) -> coverages[i] > coverages[j] ? i : j)
.orElse(-1);
if (maxIndex == -1) return null;
result = simpleNames.get(maxIndex);
}
}
if (result == null) {
Messages.log("no simple name found");
} else {
Messages.log("found " + node.toString());
}
return result;
}
public static List<SimpleName> getSimpleNameChildren(ASTNode node) {
List<SimpleName> simpleNames = new ArrayList<>();
node.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName simpleName) {
simpleNames.add(simpleName);
return super.visit(simpleName);
}
});
return simpleNames;
}
public static IBinding resolveBinding(SimpleName node) {
IBinding binding = node.resolveBinding();
if (binding == null) return null;
// Fix constructor call/declaration being resolved as type
if (binding.getKind() == IBinding.TYPE) {
ASTNode context = node;
// Go up until we find non Name or Type node
// stop if context is type argument (parent is also Name/Type, but unrelated)
while (isNameOrType(context) &&
!context.getLocationInParent().getId().equals("typeArguments")) {
context = context.getParent();
}
switch (context.getNodeType()) {
case ASTNode.METHOD_DECLARATION:
MethodDeclaration decl = (MethodDeclaration) context;
if (decl.isConstructor()) {
binding = decl.resolveBinding();
}
break;
case ASTNode.CLASS_INSTANCE_CREATION:
ClassInstanceCreation cic = (ClassInstanceCreation) context;
binding = cic.resolveConstructorBinding();
break;
}
}
if (binding == null) return null;
// Normalize parametrized and raw bindings into generic bindings
switch (binding.getKind()) {
case IBinding.TYPE:
ITypeBinding type = (ITypeBinding) binding;
if (type.isParameterizedType() || type.isRawType()) {
binding = type.getErasure();
}
break;
case IBinding.METHOD:
IMethodBinding method = (IMethodBinding) binding;
ITypeBinding declaringClass = method.getDeclaringClass();
if (declaringClass.isParameterizedType() ||
declaringClass.isRawType()) {
IMethodBinding[] methods = declaringClass.getErasure().getDeclaredMethods();
IMethodBinding generic = Arrays.stream(methods)
.filter(method::overrides)
.findAny().orElse(null);
if (generic != null) method = generic;
}
if (method.isParameterizedMethod() || method.isRawMethod()) {
method = method.getMethodDeclaration();
}
binding = method;
break;
}
return binding;
}
public static boolean isNameOrType(ASTNode node) {
return node instanceof Name || node instanceof Type;
}
protected static List<SimpleName> findAllOccurrences(ASTNode root, String bindingKey) {
List<SimpleName> occurences = new ArrayList<>();
root.getRoot().accept(new ASTVisitor() {
@Override
public boolean visit(SimpleName name) {
IBinding binding = resolveBinding(name);
if (binding != null && bindingKey.equals(binding.getKey())) {
occurences.add(name);
}
return super.visit(name);
}
});
return occurences;
}
}
@@ -0,0 +1,380 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-18 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.mode.java.pdex;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
// TODO when building the label in some variants in this file,
// getReturnType2() is used instead of getReturnType().
// need to check whether that's identical in how it performs,
// and if so, use makeLabel() and makeCompletion() more [fry 180326]
// https://help.eclipse.org/neon/index.jsp?topic=%2Forg.eclipse.jdt.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fjdt%2Fcore%2Fdom%2FMethodDeclaration.html
public class CompletionCandidate implements Comparable<CompletionCandidate> {
private final String elementName;
private final String label; // the toString value
private final String completion;
private final Object wrappedObject;
private final int type;
static final int PREDEF_CLASS = 0;
static final int PREDEF_FIELD = 1;
static final int PREDEF_METHOD = 2;
static final int LOCAL_CLASS = 3;
static final int LOCAL_METHOD = 4;
static final int LOCAL_FIELD = 5;
static final int LOCAL_VAR = 6;
CompletionCandidate(Method method) {
// return value ignored? [fry 180326]
method.getDeclaringClass().getName();
elementName = method.getName();
label = makeLabel(method);
completion = makeCompletion(method);
type = PREDEF_METHOD;
wrappedObject = method;
}
CompletionCandidate(SingleVariableDeclaration svd) {
completion = svd.getName().toString();
elementName = svd.getName().toString();
type = (svd.getParent() instanceof FieldDeclaration) ?
LOCAL_FIELD : LOCAL_VAR;
label = svd.getName() + " : " + svd.getType();
wrappedObject = svd;
}
CompletionCandidate(VariableDeclarationFragment vdf) {
completion = vdf.getName().toString();
elementName = vdf.getName().toString();
type = (vdf.getParent() instanceof FieldDeclaration) ?
LOCAL_FIELD : LOCAL_VAR;
label = vdf.getName() + " : " + CompletionGenerator.extracTypeInfo2(vdf);
wrappedObject = vdf;
}
CompletionCandidate(MethodDeclaration method) {
elementName = method.getName().toString();
type = LOCAL_METHOD;
@SuppressWarnings("unchecked")
List<ASTNode> params = (List<ASTNode>)
method.getStructuralProperty(MethodDeclaration.PARAMETERS_PROPERTY);
{ // label
StringBuilder labelBuilder = new StringBuilder(elementName);
labelBuilder.append('(');
for (int i = 0; i < params.size(); i++) {
labelBuilder.append(params.get(i).toString());
if (i < params.size() - 1) {
labelBuilder.append(',');
}
}
labelBuilder.append(')');
if (method.getReturnType2() != null) {
labelBuilder.append(" : ");
labelBuilder.append(method.getReturnType2());
}
label = labelBuilder.toString();
}
{ // completion
StringBuilder compBuilder = new StringBuilder(elementName);
compBuilder.append('(');
for (int i = 0; i < params.size(); i++) {
if (i < params.size() - 1) {
compBuilder.append(',');
}
}
if (params.size() == 1) {
compBuilder.append(' ');
}
compBuilder.append(')');
completion = compBuilder.toString();
}
wrappedObject = method;
}
CompletionCandidate(TypeDeclaration td) {
type = LOCAL_CLASS;
elementName = td.getName().toString();
label = elementName;
completion = elementName;
wrappedObject = td;
}
CompletionCandidate(Field f) {
f.getDeclaringClass().getName();
elementName = f.getName();
type = PREDEF_FIELD;
label = "<html>" +
f.getName() + " : " +
f.getType().getSimpleName() + " - " +
"<font color=#777777>" +
f.getDeclaringClass().getSimpleName() +
"</font></html>";
completion = elementName;
wrappedObject = f;
}
CompletionCandidate(String elementName, String label,
String completion, int type) {
this(elementName, label, completion, type, null);
}
private CompletionCandidate(String elementName, String label,
String completion, int type,
Object wrappedObject) {
this.elementName = elementName;
this.label = label;
this.completion = completion;
this.type = type;
this.wrappedObject = wrappedObject;
}
Object getWrappedObject() {
return wrappedObject;
}
public String getElementName() {
return elementName;
}
public String getCompletionString() {
return completion;
}
public int getType() {
return type;
}
public String getLabel() {
return label;
}
// TODO this is gross [fry 180326]
/*
private String getNoHtmlLabel(){
if (!label.contains("<html>")) {
return label;
} else {
StringBuilder ans = new StringBuilder(label);
while (ans.indexOf("<") > -1) {
int a = ans.indexOf("<"), b = ans.indexOf(">");
if (a > b) break;
ans.replace(a, b+1, "");
}
return ans.toString();
}
}
*/
boolean startsWith(String newWord) {
// System.out.println("checking " + newWord);
// return getNoHtmlLabel().toLowerCase().startsWith(newWord);
// this seems to be elementName in all cases [fry 180326]
return elementName.startsWith(newWord);
}
CompletionCandidate withLabelAndCompString(String withLabel,
String withCompletion) {
return new CompletionCandidate(elementName,
withLabel, withCompletion,
type, wrappedObject);
}
CompletionCandidate withRegeneratedCompString() {
if (wrappedObject instanceof MethodDeclaration) {
MethodDeclaration method = (MethodDeclaration)wrappedObject;
@SuppressWarnings("unchecked")
List<ASTNode> params = (List<ASTNode>)
method.getStructuralProperty(MethodDeclaration.PARAMETERS_PROPERTY);
// build the html label
StringBuilder labelBuilder = new StringBuilder(elementName);
labelBuilder.append('(');
for (int i = 0; i < params.size(); i++) {
labelBuilder.append(params.get(i));
if (i < params.size() - 1) {
labelBuilder.append(',');
}
}
labelBuilder.append(')');
if (method.getReturnType2() != null) {
labelBuilder.append(" : ");
labelBuilder.append(method.getReturnType2());
}
// build the completion str
StringBuilder compBuilder = new StringBuilder();
compBuilder.append(method.getName());
compBuilder.append('(');
for (int i = 0; i < params.size(); i++) {
if (i < params.size() - 1) {
compBuilder.append(',');
}
}
if (params.size() == 1) {
compBuilder.append(' ');
}
compBuilder.append(')');
return withLabelAndCompString(labelBuilder.toString(), compBuilder.toString());
} else if (wrappedObject instanceof Method) {
Method method = (Method) wrappedObject;
Class<?>[] types = method.getParameterTypes();
// build html label
StringBuilder labelBuilder = new StringBuilder();
labelBuilder.append("<html>");
labelBuilder.append(method.getName());
labelBuilder.append('(');
for (int i = 0; i < types.length; i++) {
labelBuilder.append(types[i].getSimpleName());
if (i < types.length - 1) {
labelBuilder.append(',');
}
}
labelBuilder.append(')');
if (method.getReturnType() != null) {
labelBuilder.append(" : " + method.getReturnType().getSimpleName());
}
labelBuilder.append(" - <font color=#777777>");
labelBuilder.append(method.getDeclaringClass().getSimpleName());
labelBuilder.append("</font>");
labelBuilder.append("</html>");
// make completion string
StringBuilder compBuilder = new StringBuilder(method.getName());
compBuilder.append('(');
for (int i = 0; i < types.length; i++) {
if (i < types.length - 1) {
compBuilder.append(',');
}
}
if (types.length == 1) {
compBuilder.append(' ');
}
compBuilder.append(')');
return withLabelAndCompString(labelBuilder.toString(), compBuilder.toString());
}
// fall-through silently does nothing? [fry 180326]
return this;
}
static private String makeLabel(Method method) {
Class<?>[] types = method.getParameterTypes();
StringBuilder labelBuilder = new StringBuilder();
labelBuilder.append("<html>");
labelBuilder.append(method.getName());
labelBuilder.append('(');
for (int i = 0; i < types.length; i++) {
labelBuilder.append(types[i].getSimpleName());
if (i < types.length - 1) {
labelBuilder.append(',');
}
}
labelBuilder.append(")");
if (method.getReturnType() != null) {
labelBuilder.append(" : ");
labelBuilder.append(method.getReturnType().getSimpleName());
}
labelBuilder.append(" - <font color=#777777>");
labelBuilder.append(method.getDeclaringClass().getSimpleName());
labelBuilder.append("</font>");
labelBuilder.append("</html>");
return labelBuilder.toString();
}
static private String makeCompletion(Method method) {
Class<?>[] types = method.getParameterTypes();
StringBuilder compBuilder = new StringBuilder();
compBuilder.append(method.getName());
compBuilder.append('(');
for (int i = 0; i < types.length; i++) {
if (i < types.length - 1) {
compBuilder.append(','); // wtf? [fry 180326]
}
}
if (types.length == 1) {
compBuilder.append(' ');
}
compBuilder.append(')');
return compBuilder.toString();
}
@Override
public int compareTo(CompletionCandidate cc) {
if (type != cc.getType()) {
return cc.getType() - type;
}
return elementName.compareTo(cc.getElementName());
}
public String toString() {
return label;
}
}
@@ -0,0 +1,595 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.pdex;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.text.BadLocationException;
import processing.app.Base;
import processing.app.Messages;
import processing.app.Mode;
import processing.app.syntax.JEditTextArea;
import processing.app.ui.Toolkit;
import processing.mode.java.JavaEditor;
public class CompletionPanel {
/**
* The completion list generated by ASTGenerator
*/
private JList<CompletionCandidate> completionList;
/**
* The popup menu in which the suggestion list is shown
*/
private JPopupMenu popupMenu;
/**
* Partial word which triggered the code completion and which needs to be completed
*/
private String subWord;
/**
* Position where the completion has to be inserted
*/
private int insertionPosition;
private JavaTextArea textarea;
/**
* Scroll pane in which the completion list is displayed
*/
private JScrollPane scrollPane;
protected JavaEditor editor;
static protected final int MOUSE_COMPLETION = 10, KEYBOARD_COMPLETION = 20;
private boolean horizontalScrollBarVisible = false;
static public ImageIcon classIcon;
static public ImageIcon fieldIcon;
static public ImageIcon methodIcon;
static public ImageIcon localVarIcon;
static Color selectionBgColor;
static Color textColor;
/**
* Triggers the completion popup
* @param textarea
* @param position - insertion position(caret pos)
* @param subWord - Partial word which triggered the code completion and which needs to be completed
* @param items - completion candidates
* @param location - Point location where popup list is to be displayed
* @param dedit
*/
public CompletionPanel(final JEditTextArea textarea,
int position, String subWord,
DefaultListModel<CompletionCandidate> items,
final Point location, JavaEditor editor) {
this.textarea = (JavaTextArea) textarea;
this.editor = editor;
this.insertionPosition = position;
if (subWord.indexOf('.') != -1 && subWord.indexOf('.') != subWord.length()-1) {
this.subWord = subWord.substring(subWord.lastIndexOf('.') + 1);
} else {
this.subWord = subWord;
}
if (classIcon == null) {
Mode mode = editor.getMode();
File dir = new File(mode.getFolder(), "theme/completion");
classIcon = Toolkit.getIconX(dir, "class_obj");
methodIcon = Toolkit.getIconX(dir, "methpub_obj");
fieldIcon = Toolkit.getIconX(dir, "field_protected_obj");
localVarIcon = Toolkit.getIconX(dir, "field_default_obj");
//selectionBgColor = mode.getColor(""); // no theme.txt for Java Mode
selectionBgColor = new Color(0xffF0F0F0);
textColor = new Color(0xff222222);
}
popupMenu = new JPopupMenu();
popupMenu.removeAll();
popupMenu.setOpaque(false);
popupMenu.setBorder(null);
scrollPane = new JScrollPane();
// styleScrollPane();
//scrollPane.setViewportView(completionList = createSuggestionList(position, items));
completionList = new JList<CompletionCandidate>(items) {
{
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setSelectedIndex(0);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
insertSelection(MOUSE_COMPLETION);
setInvisible();
}
}
});
setCellRenderer(new CustomListRenderer());
setFocusable(false);
setFont(Toolkit.getSansFont(12, Font.PLAIN));
}
};
scrollPane.setViewportView(completionList);
// remove an ugly multi-line border around it
scrollPane.setBorder(null);
popupMenu.add(scrollPane, BorderLayout.CENTER);
popupMenu.setPopupSize(calcWidth(), calcHeight(items.getSize())); //TODO: Eradicate this evil
popupMenu.setFocusable(false);
// TODO: Update JavaDoc to completionList.getSelectedValue()
popupMenu.show(textarea, location.x, textarea.getBaseline(0, 0) + location.y);
//log("Suggestion shown: " + System.currentTimeMillis());
}
/*
private void styleScrollPane() {
String laf = UIManager.getLookAndFeel().getID();
if (!laf.equals("Nimbus") && !laf.equals("Windows")) return;
String thumbColor = null;
if (laf.equals("Nimbus")) {
UIDefaults defaults = new UIDefaults();
defaults.put("PopupMenu.contentMargins", new InsetsUIResource(0, 0, 0, 0));
defaults.put("ScrollPane[Enabled].borderPainter", new Painter<JComponent>() {
public void paint(Graphics2D g, JComponent t, int w, int h) {}
});
popupMenu.putClientProperty("Nimbus.Overrides", defaults);
scrollPane.putClientProperty("Nimbus.Overrides", defaults);
thumbColor = "nimbusBlueGrey";
} else if (laf.equals("Windows")) {
thumbColor = "ScrollBar.thumbShadow";
}
scrollPane.getHorizontalScrollBar().setPreferredSize(new Dimension(Integer.MAX_VALUE, 8));
scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(8, Integer.MAX_VALUE));
scrollPane.getHorizontalScrollBar().setUI(new CompletionScrollBarUI(thumbColor));
scrollPane.getVerticalScrollBar().setUI(new CompletionScrollBarUI(thumbColor));
}
private static class CompletionScrollBarUI extends BasicScrollBarUI {
private String thumbColorName;
protected CompletionScrollBarUI(String thumbColorName) {
this.thumbColorName = thumbColorName;
}
@Override
protected void paintThumb(Graphics g, JComponent c, Rectangle trackBounds) {
g.setColor((Color) UIManager.get(thumbColorName));
g.fillRect(trackBounds.x, trackBounds.y, trackBounds.width, trackBounds.height);
}
@Override
protected JButton createDecreaseButton(int orientation) {
return createZeroButton();
}
@Override
protected JButton createIncreaseButton(int orientation) {
return createZeroButton();
}
static private JButton createZeroButton() {
JButton jbutton = new JButton();
jbutton.setPreferredSize(new Dimension(0, 0));
jbutton.setMinimumSize(new Dimension(0, 0));
jbutton.setMaximumSize(new Dimension(0, 0));
return jbutton;
}
}
*/
public boolean isVisible() {
return popupMenu.isVisible();
}
public void setInvisible() {
popupMenu.setVisible(false);
}
/**
* Dynamic height of completion panel depending on item count
*/
private int calcHeight(int itemCount) {
int maxHeight = 250;
FontMetrics fm = textarea.getGraphics().getFontMetrics();
float itemHeight = Math.max((fm.getHeight() + (fm.getDescent()) * 0.5f),
classIcon.getIconHeight() * 1.2f);
if (horizontalScrollBarVisible) {
itemCount++;
}
if (itemCount < 4) {
itemHeight *= 1.3f; //Sorry, but it works.
}
float h = itemHeight * (itemCount);
if (itemCount >= 4) {
h += itemHeight * 0.3; // a bit of offset
}
return Math.min(maxHeight, (int) h); // popup menu height
}
private int calcWidth() {
int maxWidth = 300;
float min = 0;
FontMetrics fm = textarea.getGraphics().getFontMetrics();
for (int i = 0; i < completionList.getModel().getSize(); i++) {
float h = fm.stringWidth(completionList.getModel().getElementAt(i).getLabel());
min = Math.max(min, h);
}
int w = Math.min((int) min, maxWidth);
horizontalScrollBarVisible = (w == maxWidth);
w += classIcon.getIconWidth(); // add icon width too!
w += fm.stringWidth(" "); // a bit of offset
//log("popup width " + w);
return w; // popup menu width
}
/**
* Created the popup list to be displayed
* @param position
* @param items
* @return
private JList<CompletionCandidate> createSuggestionList(final int position,
final DefaultListModel<CompletionCandidate> items) {
JList<CompletionCandidate> list = new JList<CompletionCandidate>(items);
//list.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY, 1));
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setSelectedIndex(0);
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
insertSelection(MOUSE_COMPLETION);
setInvisible();
}
}
});
list.setCellRenderer(new CustomListRenderer());
list.setFocusable(false);
return list;
}
*/
/*
// possibly defunct
private boolean updateList(final DefaultListModel<CompletionCandidate> items, String newSubword,
final Point location, int position) {
this.subWord = new String(newSubword);
if (subWord.indexOf('.') != -1)
this.subWord = subWord.substring(subWord.lastIndexOf('.') + 1);
insertionPosition = position;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
scrollPane.getViewport().removeAll();
completionList.setModel(items);
completionList.setSelectedIndex(0);
scrollPane.setViewportView(completionList);
popupMenu.setPopupSize(calcWidth(), calcHeight(items.getSize()));
//log("Suggestion updated" + System.nanoTime());
textarea.requestFocusInWindow();
popupMenu.show(textarea, location.x, textarea.getBaseline(0, 0)
+ location.y);
completionList.validate();
scrollPane.validate();
popupMenu.validate();
}
});
return true;
}
*/
/**
* Inserts the CompletionCandidate chosen from the suggestion list
* @param completionSource - whether being completed via keypress or mouse click.
* @return true - if code was successfully inserted at the caret position
*/
protected boolean insertSelection(int completionSource) {
if (completionList.getSelectedValue() != null) {
try {
// If user types 'abc.', subword becomes '.' and null is returned
String currentSubword = fetchCurrentSubword();
int currentSubwordLen =
(currentSubword == null) ? 0 : currentSubword.length();
//logE(currentSubword + " <= subword,len => " + currentSubword.length());
String selectedSuggestion =
completionList.getSelectedValue().getCompletionString();
if (currentSubword != null) {
selectedSuggestion = selectedSuggestion.substring(currentSubwordLen);
} else {
currentSubword = "";
}
String completionString =
completionList.getSelectedValue().getCompletionString();
if (selectedSuggestion.endsWith(" )")) { // the case of single param methods
// selectedSuggestion = ")";
if (completionString.endsWith(" )")) {
completionString =
completionString.substring(0, completionString.length() - 2) + ")";
}
}
boolean mouseClickOnOverloadedMethods = false;
if (completionSource == MOUSE_COMPLETION) {
// The case of overloaded methods, displayed as 'foo(...)'
// They have completion strings as 'foo('. See #2755
if (completionString.endsWith("(")) {
mouseClickOnOverloadedMethods = true;
}
}
Messages.loge(subWord + " <= subword, Inserting suggestion=> " +
selectedSuggestion + " Current sub: " + currentSubword);
if (currentSubword.length() > 0) {
textarea.getDocument().remove(insertionPosition - currentSubwordLen,
currentSubwordLen);
}
textarea.getDocument().insertString(insertionPosition - currentSubwordLen,
completionString, null);
if (selectedSuggestion.endsWith(")") && !selectedSuggestion.endsWith("()")) {
// place the caret between '( and first ','
int x = selectedSuggestion.indexOf(',');
if (x == -1) {
// the case of single param methods, containing no ','
textarea.setCaretPosition(textarea.getCaretPosition() - 1); // just before ')'
} else {
textarea.setCaretPosition(insertionPosition + x);
}
}
Messages.log("Suggestion inserted: " + System.currentTimeMillis());
if (completionList.getSelectedValue().getLabel().contains("...")) {
// log("No hide");
// Why not hide it? Coz this is the case of
// overloaded methods. See #2755
} else {
setInvisible();
}
if (mouseClickOnOverloadedMethods) {
// See #2755
((JavaTextArea) editor.getTextArea()).fetchPhrase();
}
return true;
} catch (BadLocationException e1) {
e1.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
setInvisible();
}
return false;
}
private String fetchCurrentSubword() {
//log("Entering fetchCurrentSubword");
JEditTextArea ta = editor.getTextArea();
int off = ta.getCaretPosition();
//log2("off " + off);
if (off < 0)
return null;
int line = ta.getCaretLine();
if (line < 0)
return null;
String s = ta.getLineText(line);
//log2("lin " + line);
//log2(s + " len " + s.length());
int x = ta.getCaretPosition() - ta.getLineStartOffset(line) - 1, x1 = x - 1;
if (x >= s.length() || x < 0)
return null; //TODO: Does this check cause problems? Verify.
if (Base.DEBUG) System.out.print(" x char: " + s.charAt(x));
//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);
return word;
}
//log("fetchCurrentSubword 1 " + word);
if(word.equals(".")) return null; // If user types 'abc.', subword becomes '.'
// if (keyChar == KeyEvent.VK_BACK_SPACE || keyChar == KeyEvent.VK_DELETE)
// ; // accepted these keys
// else if (!(Character.isLetterOrDigit(keyChar) || keyChar == '_' || keyChar == '$'))
// return null;
int i = 0;
while (true) {
i++;
//TODO: currently works on single line only. "a. <new line> 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) == '_') {
word = s.charAt(x1--) + word;
} else {
break;
}
} else {
break;
}
if (i > 200) {
// time out!
break;
}
}
// if (keyChar != KeyEvent.CHAR_UNDEFINED)
//log("fetchCurrentSubword 2 " + word);
if (Character.isDigit(word.charAt(0)))
return null;
word = word.trim();
if (word.endsWith("."))
word = word.substring(0, word.length() - 1);
//log("fetchCurrentSubword 3 " + word);
//showSuggestionLater();
return word;
//}
}
/**
* When up arrow key is pressed, moves the highlighted selection up in the list
*/
protected void moveUp() {
if (completionList.getSelectedIndex() == 0) {
scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum());
selectIndex(completionList.getModel().getSize() - 1);
} else {
int index = Math.max(completionList.getSelectedIndex() - 1, 0);
selectIndex(index);
int step = scrollPane.getVerticalScrollBar().getMaximum()
/ completionList.getModel().getSize();
scrollPane.getVerticalScrollBar().setValue(scrollPane
.getVerticalScrollBar()
.getValue()
- step);
// TODO: update JavaDoc to completionList.getSelectedValue()
}
}
/**
* When down arrow key is pressed, moves the highlighted selection down in the list
*/
protected void moveDown() {
if (completionList.getSelectedIndex() == completionList.getModel().getSize() - 1) {
scrollPane.getVerticalScrollBar().setValue(0);
selectIndex(0);
} else {
int index = Math.min(completionList.getSelectedIndex() + 1,
completionList.getModel().getSize() - 1);
selectIndex(index);
// TODO: update JavaDoc to completionList.getSelectedValue()
int step = scrollPane.getVerticalScrollBar().getMaximum() / completionList.getModel().getSize();
scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getValue() + step);
}
}
private void selectIndex(int index) {
completionList.setSelectedIndex(index);
}
/**
* Custom cell renderer to display icons along with the completion candidates
* @author Manindra Moharana <me@mkmoharana.com>
*
*/
private static class CustomListRenderer extends javax.swing.DefaultListCellRenderer {
public Component getListCellRendererComponent(JList<?> list, Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value,
index,
isSelected,
cellHasFocus);
if (isSelected) {
label.setBackground(selectionBgColor);
}
label.setForeground(textColor);
if (value instanceof CompletionCandidate) {
CompletionCandidate cc = (CompletionCandidate) value;
switch (cc.getType()) {
case CompletionCandidate.LOCAL_VAR:
label.setIcon(localVarIcon);
break;
case CompletionCandidate.LOCAL_FIELD:
case CompletionCandidate.PREDEF_FIELD:
label.setIcon(fieldIcon);
break;
case CompletionCandidate.LOCAL_METHOD:
case CompletionCandidate.PREDEF_METHOD:
label.setIcon(methodIcon);
break;
case CompletionCandidate.LOCAL_CLASS:
case CompletionCandidate.PREDEF_CLASS:
label.setIcon(classIcon);
break;
default:
Messages.log("(CustomListRenderer)Unknown CompletionCandidate type " + cc.getType());
break;
}
} else {
Messages.log("(CustomListRenderer)Unknown CompletionCandidate object " + value);
}
return label;
}
}
}
@@ -0,0 +1,139 @@
package processing.mode.java.pdex;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.Consumer;
import javax.swing.JDialog;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.WindowConstants;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import processing.app.Messages;
import processing.app.ui.ZoomTreeCellRenderer;
import processing.mode.java.JavaEditor;
import processing.mode.java.pdex.PreprocessedSketch.SketchInterval;
class DebugTree {
final JDialog window;
final JTree tree;
final Consumer<PreprocessedSketch> updateListener;
DebugTree(JavaEditor editor, PreprocessingService pps) {
updateListener = this::buildAndUpdateTree;
window = new JDialog(editor);
tree = new JTree() {
@Override
public String convertValueToText(Object value, boolean selected,
boolean expanded, boolean leaf,
int row, boolean hasFocus) {
if (value instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) value;
Object o = treeNode.getUserObject();
if (o instanceof ASTNode) {
ASTNode node = (ASTNode) o;
return CompletionGenerator.getNodeAsString(node);
}
}
return super.convertValueToText(value, selected, expanded, leaf, row, hasFocus);
}
};
tree.setCellRenderer(new ZoomTreeCellRenderer(editor.getMode()));
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
pps.unregisterListener(updateListener);
tree.setModel(null);
}
});
window.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
window.setBounds(new Rectangle(680, 100, 460, 620));
window.setTitle("AST View - " + editor.getSketch().getName());
JScrollPane sp = new JScrollPane();
sp.setViewportView(tree);
window.add(sp);
pps.whenDone(updateListener);
pps.registerListener(updateListener);
tree.addTreeSelectionListener(e -> {
if (tree.getLastSelectedPathComponent() != null) {
DefaultMutableTreeNode tnode =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (tnode.getUserObject() instanceof ASTNode) {
ASTNode node = (ASTNode) tnode.getUserObject();
pps.whenDone(ps -> {
SketchInterval si = ps.mapJavaToSketch(node);
if (!ps.inRange(si)) return;
EventQueue.invokeLater(() -> {
editor.highlight(si.tabIndex, si.startTabOffset, si.stopTabOffset);
});
});
}
}
});
}
void dispose() {
if (window != null) {
window.dispose();
}
}
// Thread: worker
void buildAndUpdateTree(PreprocessedSketch ps) {
CompilationUnit cu = ps.compilationUnit;
if (cu.types().isEmpty()){
Messages.loge("No Type found in CU");
return;
}
Deque<DefaultMutableTreeNode> treeNodeStack = new ArrayDeque<>();
ASTNode type0 = (ASTNode) cu.types().get(0);
type0.accept(new ASTVisitor() {
@Override
public boolean preVisit2(ASTNode node) {
treeNodeStack.push(new DefaultMutableTreeNode(node));
return super.preVisit2(node);
}
@Override
public void postVisit(ASTNode node) {
if (treeNodeStack.size() > 1) {
DefaultMutableTreeNode treeNode = treeNodeStack.pop();
treeNodeStack.peek().add(treeNode);
}
}
});
DefaultMutableTreeNode codeTree = treeNodeStack.pop();
EventQueue.invokeLater(() -> {
if (tree.hasFocus() || window.hasFocus()) {
return;
}
tree.setModel(new DefaultTreeModel(codeTree));
((DefaultTreeModel) tree.getModel()).reload();
tree.validate();
if (!window.isVisible()) {
window.setVisible(true);
}
});
}
}
@@ -0,0 +1,327 @@
package processing.mode.java.pdex;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.jdt.core.compiler.IProblem;
import com.google.classpath.ClassPath;
import com.google.classpath.ClassPathFactory;
import com.google.classpath.RegExpResourceFilter;
import processing.app.Language;
import processing.app.Problem;
import processing.mode.java.JavaEditor;
import processing.mode.java.JavaMode;
import processing.mode.java.pdex.PreprocessedSketch.SketchInterval;
class ErrorChecker {
// Delay delivering error check result after last sketch change #2677
private final static long DELAY_BEFORE_UPDATE = 650;
private ScheduledExecutorService scheduler;
private volatile ScheduledFuture<?> scheduledUiUpdate = null;
private volatile long nextUiUpdate = 0;
private volatile boolean enabled = true;
private final Consumer<PreprocessedSketch> errorHandlerListener = this::handleSketchProblems;
private JavaEditor editor;
private PreprocessingService pps;
public ErrorChecker(JavaEditor editor, PreprocessingService pps) {
this.editor = editor;
this.pps = pps;
scheduler = Executors.newSingleThreadScheduledExecutor();
this.enabled = JavaMode.errorCheckEnabled;
if (enabled) {
pps.registerListener(errorHandlerListener);
}
}
public void notifySketchChanged() {
nextUiUpdate = System.currentTimeMillis() + DELAY_BEFORE_UPDATE;
}
public void preferencesChanged() {
if (enabled != JavaMode.errorCheckEnabled) {
enabled = JavaMode.errorCheckEnabled;
if (enabled) {
pps.registerListener(errorHandlerListener);
} else {
pps.unregisterListener(errorHandlerListener);
editor.setProblemList(Collections.emptyList());
nextUiUpdate = 0;
}
}
}
public void dispose() {
if (scheduler != null) {
scheduler.shutdownNow();
}
}
private void handleSketchProblems(PreprocessedSketch ps) {
Map<String, String[]> suggCache =
JavaMode.importSuggestEnabled ? new HashMap<>() : Collections.emptyMap();
final List<Problem> problems = new ArrayList<>();
IProblem[] iproblems = ps.compilationUnit.getProblems();
{ // Check for curly quotes
List<JavaProblem> curlyQuoteProblems = checkForCurlyQuotes(ps);
problems.addAll(curlyQuoteProblems);
}
if (problems.isEmpty()) { // Check for missing braces
List<JavaProblem> missingBraceProblems = checkForMissingBraces(ps);
problems.addAll(missingBraceProblems);
}
if (problems.isEmpty()) {
AtomicReference<ClassPath> searchClassPath = new AtomicReference<>(null);
List<Problem> cuProblems = Arrays.stream(iproblems)
// Filter Warnings if they are not enabled
.filter(iproblem -> !(iproblem.isWarning() && !JavaMode.warningsEnabled))
// Hide a useless error which is produced when a line ends with
// an identifier without a semicolon. "Missing a semicolon" is
// also produced and is preferred over this one.
// (Syntax error, insert ":: IdentifierOrNew" to complete Expression)
// See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=405780
.filter(iproblem -> !iproblem.getMessage()
.contains("Syntax error, insert \":: IdentifierOrNew\""))
// Transform into our Problems
.map(iproblem -> {
JavaProblem p = convertIProblem(iproblem, ps);
// Handle import suggestions
if (p != null && JavaMode.importSuggestEnabled && isUndefinedTypeProblem(iproblem)) {
ClassPath cp = searchClassPath.updateAndGet(prev -> prev != null ?
prev : new ClassPathFactory().createFromPaths(ps.searchClassPathArray));
String[] s = suggCache.computeIfAbsent(iproblem.getArguments()[0],
name -> getImportSuggestions(cp, name));
p.setImportSuggestions(s);
}
return p;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
problems.addAll(cuProblems);
}
if (scheduledUiUpdate != null) {
scheduledUiUpdate.cancel(true);
}
// Update UI after a delay. See #2677
long delay = nextUiUpdate - System.currentTimeMillis();
Runnable uiUpdater = () -> {
if (nextUiUpdate > 0 && System.currentTimeMillis() >= nextUiUpdate) {
EventQueue.invokeLater(() -> editor.setProblemList(problems));
}
};
scheduledUiUpdate = scheduler.schedule(uiUpdater, delay,
TimeUnit.MILLISECONDS);
}
static private JavaProblem convertIProblem(IProblem iproblem, PreprocessedSketch ps) {
SketchInterval in = ps.mapJavaToSketch(iproblem);
if (in == SketchInterval.BEFORE_START) return null;
String badCode = ps.getPdeCode(in);
int line = ps.tabOffsetToTabLine(in.tabIndex, in.startTabOffset);
JavaProblem p = JavaProblem.fromIProblem(iproblem, in.tabIndex, line, badCode);
p.setPDEOffsets(in.startTabOffset, in.stopTabOffset);
return p;
}
static private boolean isUndefinedTypeProblem(IProblem iproblem) {
int id = iproblem.getID();
return id == IProblem.UndefinedType ||
id == IProblem.UndefinedName ||
id == IProblem.UnresolvedVariable;
}
static private boolean isMissingBraceProblem(IProblem iproblem) {
switch (iproblem.getID()) {
case IProblem.ParsingErrorInsertToComplete: {
char brace = iproblem.getArguments()[0].charAt(0);
return brace == '{' || brace == '}';
}
case IProblem.ParsingErrorInsertTokenAfter: {
char brace = iproblem.getArguments()[1].charAt(0);
return brace == '{' || brace == '}';
}
default:
return false;
}
}
private static final Pattern CURLY_QUOTE_REGEX =
Pattern.compile("([“”‘’])", Pattern.UNICODE_CHARACTER_CLASS);
static private List<JavaProblem> checkForCurlyQuotes(PreprocessedSketch ps) {
List<JavaProblem> problems = new ArrayList<>(0);
// Go through the scrubbed code and look for curly quotes (they should not be any)
Matcher matcher = CURLY_QUOTE_REGEX.matcher(ps.scrubbedPdeCode);
while (matcher.find()) {
int pdeOffset = matcher.start();
String q = matcher.group();
int tabIndex = ps.pdeOffsetToTabIndex(pdeOffset);
int tabOffset = ps.pdeOffsetToTabOffset(tabIndex, pdeOffset);
int tabLine = ps.tabOffsetToTabLine(tabIndex, tabOffset);
String message = Language.interpolate("editor.status.bad_curly_quote", q);
JavaProblem problem = new JavaProblem(message, JavaProblem.ERROR, tabIndex, tabLine);
problem.setPDEOffsets(tabOffset, tabOffset+1);
problems.add(problem);
}
// Go through iproblems and look for problems involving curly quotes
List<JavaProblem> problems2 = new ArrayList<>(0);
IProblem[] iproblems = ps.compilationUnit.getProblems();
for (IProblem iproblem : iproblems) {
switch (iproblem.getID()) {
case IProblem.ParsingErrorDeleteToken:
case IProblem.ParsingErrorDeleteTokens:
case IProblem.ParsingErrorInvalidToken:
case IProblem.ParsingErrorReplaceTokens:
case IProblem.UnterminatedString:
SketchInterval in = ps.mapJavaToSketch(iproblem);
if (in == SketchInterval.BEFORE_START) continue;
String badCode = ps.getPdeCode(in);
matcher.reset(badCode);
while (matcher.find()) {
int offset = matcher.start();
String q = matcher.group();
int tabStart = in.startTabOffset + offset;
int tabStop = tabStart + 1;
// Prevent duplicate problems
if (problems.stream().noneMatch(p -> p.getStartOffset() == tabStart)) {
int line = ps.tabOffsetToTabLine(in.tabIndex, tabStart);
String message;
if (iproblem.getID() == IProblem.UnterminatedString) {
message = Language.interpolate("editor.status.unterm_string_curly", q);
} else {
message = Language.interpolate("editor.status.bad_curly_quote", q);
}
JavaProblem p = new JavaProblem(message, JavaProblem.ERROR, in.tabIndex, line);
p.setPDEOffsets(tabStart, tabStop);
problems2.add(p);
}
}
}
}
problems.addAll(problems2);
return problems;
}
static private List<JavaProblem> checkForMissingBraces(PreprocessedSketch ps) {
List<JavaProblem> problems = new ArrayList<>(0);
for (int tabIndex = 0; tabIndex < ps.tabStartOffsets.length; tabIndex++) {
int tabStartOffset = ps.tabStartOffsets[tabIndex];
int tabEndOffset = (tabIndex < ps.tabStartOffsets.length - 1) ?
ps.tabStartOffsets[tabIndex + 1] : ps.scrubbedPdeCode.length();
int[] braceResult = SourceUtils.checkForMissingBraces(ps.scrubbedPdeCode, tabStartOffset, tabEndOffset);
if (braceResult[0] != 0) {
JavaProblem problem =
new JavaProblem(braceResult[0] < 0
? Language.interpolate("editor.status.missing.left_curly_bracket")
: Language.interpolate("editor.status.missing.right_curly_bracket"),
JavaProblem.ERROR, tabIndex, braceResult[1]);
problem.setPDEOffsets(braceResult[3], braceResult[3] + 1);
problems.add(problem);
}
}
if (problems.isEmpty()) {
return problems;
}
int problemTabIndex = problems.get(0).getTabIndex();
IProblem missingBraceProblem = Arrays.stream(ps.compilationUnit.getProblems())
.filter(ErrorChecker::isMissingBraceProblem)
// Ignore if it is at the end of file
.filter(p -> p.getSourceEnd() + 1 < ps.javaCode.length())
// Ignore if the tab number does not match our detected tab number
.filter(p -> problemTabIndex == ps.mapJavaToSketch(p).tabIndex)
.findFirst()
.orElse(null);
// Prefer ECJ problem, shows location more accurately
if (missingBraceProblem != null) {
JavaProblem p = convertIProblem(missingBraceProblem, ps);
if (p != null) {
problems.clear();
problems.add(p);
}
}
return problems;
}
static public String[] getImportSuggestions(ClassPath cp, String className) {
className = className.replace("[", "\\[").replace("]", "\\]");
RegExpResourceFilter regf = new RegExpResourceFilter(
Pattern.compile(".*"),
Pattern.compile("(.*\\$)?" + className + "\\.class",
Pattern.CASE_INSENSITIVE));
String[] resources = cp.findResources("", regf);
return Arrays.stream(resources)
// remove ".class" suffix
.map(res -> res.substring(0, res.length() - 6))
// replace path separators with dots
.map(res -> res.replace('/', '.'))
// replace inner class separators with dots
.map(res -> res.replace('$', '.'))
// sort, prioritize clases from java. package
.sorted((o1, o2) -> {
// put java.* first, should be prioritized more
boolean o1StartsWithJava = o1.startsWith("java");
boolean o2StartsWithJava = o2.startsWith("java");
if (o1StartsWithJava != o2StartsWithJava) {
if (o1StartsWithJava) return -1;
return 1;
}
return o1.compareTo(o2);
})
.toArray(String[]::new);
}
}
@@ -0,0 +1,372 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.pdex;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblem;
import processing.app.Language;
import processing.app.Messages;
import processing.core.PApplet;
import processing.data.StringList;
public class ErrorMessageSimplifier {
/**
* Mapping between ProblemID constant and the constant name. Holds about 650
* of them. Also, this is just temporary, will be used to find the common
* error types, cos you know, identifying String names is easier than
* identifying 8 digit int constants!
* TODO: this is temporary
*/
private static TreeMap<Integer, String> constantsMap;
private static final boolean DEBUG = false;
private static final Pattern tokenRegExp = Pattern.compile("\\b token\\b");
private static void prepareConstantsList() {
constantsMap = new TreeMap<>();
Class<DefaultProblem> probClass = DefaultProblem.class;
Field f[] = probClass.getFields();
for (Field field : f) {
if (Modifier.isStatic(field.getModifiers()))
try {
if (DEBUG) {
Messages.log(field.getName() + " :" + field.get(null));
}
Object val = field.get(null);
if (val instanceof Integer) {
constantsMap.put((Integer) (val), field.getName());
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}
if (DEBUG) {
Messages.log("Total items: " + constantsMap.size());
}
}
public static String getIDName(int id) {
if (constantsMap == null) {
prepareConstantsList();
}
return constantsMap.get(id);
}
/**
* Tones down the jargon in the ecj reported errors.
*/
public static String getSimplifiedErrorMessage(IProblem iprob, String badCode) {
if (iprob == null) return null;
String args[] = iprob.getArguments();
if (DEBUG) {
Messages.log("Simplifying message: " + iprob.getMessage() +
" ID: " + getIDName(iprob.getID()));
Messages.log("Arg count: " + args.length);
for (String arg : args) {
Messages.log("Arg " + arg);
}
Messages.log("Bad code: " + badCode);
}
String result = null;
switch (iprob.getID()) {
case IProblem.ParsingError:
if (args.length > 0) {
result = Language.interpolate("editor.status.error_on", args[0]);
}
break;
case IProblem.ParsingErrorDeleteToken:
if (args.length > 0) {
if (args[0].equalsIgnoreCase("Invalid Character")) {
result = getErrorMessageForCurlyQuote(badCode);
}
}
break;
case IProblem.ParsingErrorDeleteTokens:
result = getErrorMessageForCurlyQuote(badCode);
if (result == null) {
result = Language.interpolate("editor.status.error_on", args[0]);
}
break;
case IProblem.ParsingErrorInsertToComplete:
if (args.length > 0) {
if (args[0].length() == 1) {
result = getErrorMessageForBracket(args[0].charAt(0));
} else {
if (args[0].equals("AssignmentOperator Expression")) {
result = Language.interpolate("editor.status.missing.add", "=");
} else if (args[0].equalsIgnoreCase(") Statement")) {
result = getErrorMessageForBracket(args[0].charAt(0));
} else {
result = Language.interpolate("editor.status.error_on", args[0]);
}
}
}
break;
case IProblem.ParsingErrorInvalidToken:
if (args.length > 0) {
if (args[0].equals("int")) {
if (args[1].equals("VariableDeclaratorId")) {
result = Language.text("editor.status.reserved_words");
} else {
result = Language.interpolate("editor.status.error_on", args[0]);
}
} else if (args[0].equalsIgnoreCase("Invalid Character")) {
result = getErrorMessageForCurlyQuote(badCode);
}
if (result == null) {
result = Language.interpolate("editor.status.error_on", args[0]);
}
}
break;
case IProblem.ParsingErrorInsertTokenAfter:
if (args.length > 0) {
if (args[1].length() == 1) {
result = getErrorMessageForBracket(args[1].charAt(0));
} else {
// https://github.com/processing/processing/issues/3104
if (args[1].equalsIgnoreCase("Statement")) {
result = Language.interpolate("editor.status.error_on", args[0]);
} else {
result =
Language.interpolate("editor.status.error_on", args[0]) + " " +
Language.interpolate("editor.status.missing.add", args[1]);
}
}
}
break;
case IProblem.ParsingErrorReplaceTokens:
result = getErrorMessageForCurlyQuote(badCode);
case IProblem.UndefinedConstructor:
if (args.length == 2) {
String constructorName = args[0];
// For messages such as "contructor sketch_name.ClassXYZ() is undefined", change
// constructor name to "ClassXYZ()". See #3434
if (constructorName.contains(".")) {
// arg[0] contains sketch name twice: sketch_150705a.sketch_150705a.Thing
constructorName = constructorName.substring(constructorName.indexOf('.') + 1);
constructorName = constructorName.substring(constructorName.indexOf('.') + 1);
}
String constructorArgs = removePackagePrefixes(args[args.length - 1]);
result = Language.interpolate("editor.status.undefined_constructor", constructorName, constructorArgs);
}
break;
case IProblem.UndefinedMethod:
if (args.length > 2) {
String methodName = args[args.length - 2];
String methodArgs = removePackagePrefixes(args[args.length - 1]);
result = Language.interpolate("editor.status.undefined_method", methodName, methodArgs);
}
break;
case IProblem.ParameterMismatch:
if (args.length > 3) {
// 2nd arg is method name, 3rd arg is correct param list
if (args[2].trim().length() == 0) {
// the case where no params are needed.
result = Language.interpolate("editor.status.empty_param", args[1]);
} else {
result = Language.interpolate("editor.status.wrong_param",
args[1], args[1], removePackagePrefixes(args[2]));
// String method = q(args[1]);
// String methodDef = " \"" + args[1] + "(" + getSimpleName(args[2]) + ")\"";
// result = result.replace("method", method);
// result += methodDef;
}
}
break;
case IProblem.UndefinedField:
if (args.length > 0) {
result = Language.interpolate("editor.status.undef_global_var", args[0]);
}
break;
case IProblem.UndefinedType:
if (args.length > 0) {
result = Language.interpolate("editor.status.undef_class", args[0]);
}
break;
case IProblem.UnresolvedVariable:
if (args.length > 0) {
result = Language.interpolate("editor.status.undef_var", args[0]);
}
break;
case IProblem.UndefinedName:
if (args.length > 0) {
result = Language.interpolate("editor.status.undef_name", args[0]);
}
break;
case IProblem.UnterminatedString:
if (badCode.contains("") || badCode.contains("")) {
result = Language.interpolate("editor.status.unterm_string_curly",
badCode.replaceAll("[^“”]", ""));
}
break;
case IProblem.TypeMismatch:
if (args.length > 1) {
result = Language.interpolate("editor.status.type_mismatch", args[0], args[1]);
// result = result.replace("typeA", q(args[0]));
// result = result.replace("typeB", q(args[1]));
}
break;
case IProblem.LocalVariableIsNeverUsed:
if (args.length > 0) {
result = Language.interpolate("editor.status.unused_variable", args[0]);
}
break;
case IProblem.UninitializedLocalVariable:
if (args.length > 0) {
result = Language.interpolate("editor.status.uninitialized_variable", args[0]);
}
break;
case IProblem.AssignmentHasNoEffect:
if (args.length > 0) {
result = Language.interpolate("editor.status.no_effect_assignment", args[0]);
}
break;
case IProblem.HidingEnclosingType:
if (args.length > 0) {
result = Language.interpolate("editor.status.hiding_enclosing_type", args[0]);
}
break;
}
if (result == null) {
String message = iprob.getMessage();
if (message != null) {
// Remove all instances of token
// "Syntax error on token 'blah', delete this token"
Matcher matcher = tokenRegExp.matcher(message);
message = matcher.replaceAll("");
result = message;
}
}
if (DEBUG) {
Messages.log("Simplified Error Msg: " + result);
}
return result;
}
/**
* Converts java.lang.String into String, etc
*/
static private String removePackagePrefixes(String input) {
if (!input.contains(".")) {
return input;
}
String[] names = PApplet.split(input, ',');
// List<String> names = new ArrayList<String>();
// if (inp.indexOf(',') >= 0) {
// names.addAll(Arrays.asList(inp.split(",")));
// } else {
// names.add(inp);
// }
StringList result = new StringList();
for (String name : names) {
int dot = name.lastIndexOf('.');
if (dot >= 0) {
name = name.substring(dot + 1, name.length());
}
result.append(name);
}
return result.join(", ");
}
static private String getErrorMessageForBracket(char c) {
switch (c) {
case ';': return Language.text("editor.status.missing.semicolon");
case '[': return Language.text("editor.status.missing.left_sq_bracket");
case ']': return Language.text("editor.status.missing.right_sq_bracket");
case '(': return Language.text("editor.status.missing.left_paren");
case ')': return Language.text("editor.status.missing.right_paren");
case '{': return Language.text("editor.status.missing.left_curly_bracket");
case '}': return Language.text("editor.status.missing.right_curly_bracket");
}
// This seems to be unreachable and wasn't in PDE.properties.
// I've added it for 3.0a8, but that seems gross. [fry]
return Language.interpolate("editor.status.missing.default", c);
}
/**
* @param badCode The code which may contain curly quotes
* @return Friendly error message if there is a curly quote in badCode,
* null otherwise.
*/
static private String getErrorMessageForCurlyQuote(String badCode) {
if (badCode.contains("") || badCode.contains("") ||
badCode.contains("") || badCode.contains("")) {
return Language.interpolate("editor.status.bad_curly_quote",
badCode.replaceAll("[^‘’“”]", ""));
} else return null;
}
// static private final String q(Object quotable) {
// return "\"" + quotable + "\"";
// }
// static private final String qs(Object quotable) {
// return " " + q(quotable);
// }
}
@@ -0,0 +1,127 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.pdex;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
/**
* Wrapper for import statements
*
* @author Manindra Moharana &lt;me@mkmoharana.com&gt;
*
*/
public class ImportStatement {
private static final String importKw = "import";
private static final String staticKw = "static";
// private boolean isClass;
private boolean isStarred;
private boolean isStatic;
/**
* Full class name of the import with all packages
* Ends with star for starred imports
*/
private String className;
/**
* Name of the package e.g. everything before last dot
*/
private String packageName;
private ImportStatement() { }
public static ImportStatement wholePackage(String pckg) {
ImportStatement is = new ImportStatement();
is.packageName = pckg;
is.className = "*";
is.isStarred = true;
return is;
}
public static ImportStatement singleClass(String cls) {
ImportStatement is = new ImportStatement();
int lastDot = cls.lastIndexOf('.');
is.className = lastDot >= 0 ? cls.substring(lastDot+1) : cls;
is.packageName = lastDot >= 0 ? cls.substring(0, lastDot) : "";
// is.isClass = true;
return is;
}
public static ImportStatement parse(String importString) {
Matcher matcher = SourceUtils.IMPORT_REGEX_NO_KEYWORD.matcher(importString);
if (!matcher.find()) return null;
return parse(matcher.toMatchResult());
}
public static ImportStatement parse(MatchResult match) {
ImportStatement is = new ImportStatement();
is.isStatic = match.group(2) != null;
String pckg = match.group(3);
pckg = (pckg == null) ? "" : pckg.replaceAll("\\s","");
is.packageName = pckg.endsWith(".") ?
pckg.substring(0, pckg.length()-1) :
pckg;
is.className = match.group(4);
is.isStarred = is.className.equals("*");
return is;
}
public String getFullSourceLine() {
return importKw + " " + (isStatic ? (staticKw + " ") : "") + packageName + "." + className + ";";
}
public String getFullClassName(){
return packageName + "." + className;
}
public String getClassName(){
return className;
}
public String getPackageName(){
return packageName;
}
public boolean isStarredImport() {
return isStarred;
}
public boolean isStaticImport() {
return isStatic;
}
public boolean isSameAs(ImportStatement is) {
return packageName.equals(is.packageName) &&
className.equals(is.className) &&
isStatic == is.isStatic;
}
}
@@ -0,0 +1,200 @@
package processing.mode.java.pdex;
import java.awt.EventQueue;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.function.Predicate;
import javax.swing.JMenuItem;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import processing.app.Language;
import processing.app.Messages;
import processing.app.Platform;
import processing.mode.java.JavaEditor;
import processing.mode.java.JavaMode;
import processing.mode.java.pdex.PreprocessedSketch.SketchInterval;
import static processing.mode.java.pdex.ASTUtils.getSimpleNameAt;
import static processing.mode.java.pdex.ASTUtils.resolveBinding;
class InspectMode {
final JavaEditor editor;
final PreprocessingService pps;
final ShowUsage usage;
boolean inspectModeEnabled;
boolean isMouse1Down;
boolean isMouse2Down;
boolean isHotkeyDown;
Predicate<MouseEvent> mouseEventHotkeyTest = Platform.isMacOS() ?
InputEvent::isMetaDown : InputEvent::isControlDown;
Predicate<KeyEvent> keyEventHotkeyTest = Platform.isMacOS() ?
e -> e.getKeyCode() == KeyEvent.VK_META :
e -> e.getKeyCode() == KeyEvent.VK_CONTROL;
InspectMode(JavaEditor editor, PreprocessingService pps, ShowUsage usage) {
this.editor = editor;
this.pps = pps;
this.usage = usage;
// Add listeners
JMenuItem showUsageItem = new JMenuItem(Language.text("editor.popup.jump_to_declaration"));
showUsageItem.addActionListener(e -> handleInspect());
editor.getTextArea().getRightClickPopup().add(showUsageItem);
editor.getJavaTextArea().getPainter().addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
isMouse1Down = isMouse1Down || (e.getButton() == MouseEvent.BUTTON1);
isMouse2Down = isMouse2Down || (e.getButton() == MouseEvent.BUTTON2);
}
@Override
public void mouseReleased(MouseEvent e) {
boolean releasingMouse1 = e.getButton() == MouseEvent.BUTTON1;
boolean releasingMouse2 = e.getButton() == MouseEvent.BUTTON2;
if (JavaMode.inspectModeHotkeyEnabled && inspectModeEnabled &&
isMouse1Down && releasingMouse1) {
handleInspect(e);
} else if (!inspectModeEnabled && isMouse2Down && releasingMouse2) {
handleInspect(e);
}
isMouse1Down = isMouse1Down && !releasingMouse1;
isMouse2Down = isMouse2Down && !releasingMouse2;
}
});
editor.getJavaTextArea().getPainter().addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (editor.isSelectionActive()) {
// Mouse was dragged too much, disable
inspectModeEnabled = false;
// Cancel possible mouse 2 press
isMouse2Down = false;
}
}
@Override
public void mouseMoved(MouseEvent e) {
isMouse1Down = false;
isMouse2Down = false;
isHotkeyDown = mouseEventHotkeyTest.test(e);
inspectModeEnabled = isHotkeyDown;
}
});
editor.getJavaTextArea().addMouseWheelListener(new MouseAdapter() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
// Editor was scrolled while mouse 1 was pressed, disable
if (isMouse1Down) inspectModeEnabled = false;
}
});
editor.getJavaTextArea().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
isHotkeyDown = isHotkeyDown || keyEventHotkeyTest.test(e);
// Enable if hotkey was just pressed and mouse 1 is not down
inspectModeEnabled = inspectModeEnabled || (!isMouse1Down && isHotkeyDown);
}
@Override
public void keyReleased(KeyEvent e) {
isHotkeyDown = isHotkeyDown && !keyEventHotkeyTest.test(e);
// Disable if hotkey was just released
inspectModeEnabled = inspectModeEnabled && isHotkeyDown;
}
});
}
void handleInspect() {
int off = editor.getSelectionStart();
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleInspect(ps, tabIndex, off));
}
// Thread: EDT
void handleInspect(MouseEvent evt) {
int off = editor.getJavaTextArea().xyToOffset(evt.getX(), evt.getY());
if (off < 0) return;
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleInspect(ps, tabIndex, off));
}
// Thread: worker
private void handleInspect(PreprocessedSketch ps, int tabIndex, int offset) {
ASTNode root = ps.compilationUnit;
int javaOffset = ps.tabOffsetToJavaOffset(tabIndex, offset);
SimpleName simpleName = getSimpleNameAt(root, javaOffset, javaOffset);
if (simpleName == null) {
Messages.log("no simple name found at click location");
return;
}
IBinding binding = resolveBinding(simpleName);
if (binding == null) {
Messages.log("binding not resolved");
return;
}
String key = binding.getKey();
ASTNode decl = ps.compilationUnit.findDeclaringNode(key);
if (decl == null) {
Messages.log("decl not found, showing usage instead");
usage.findUsageAndUpdateTree(ps, binding);
return;
}
SimpleName declName = null;
switch (binding.getKind()) {
case IBinding.TYPE: declName = ((TypeDeclaration) decl).getName(); break;
case IBinding.METHOD: declName = ((MethodDeclaration) decl).getName(); break;
case IBinding.VARIABLE: declName = ((VariableDeclaration) decl).getName(); break;
}
if (declName == null) {
Messages.log("decl name not found " + decl);
return;
}
if (declName.equals(simpleName)) {
usage.findUsageAndUpdateTree(ps, binding);
} else {
Messages.log("found declaration, offset " + decl.getStartPosition() + ", name: " + declName);
SketchInterval si = ps.mapJavaToSketch(declName);
if (!ps.inRange(si)) return;
EventQueue.invokeLater(() -> {
editor.highlight(si.tabIndex, si.startTabOffset, si.stopTabOffset);
});
}
}
void dispose() {
// Nothing to do
}
}
@@ -0,0 +1,144 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.pdex;
import org.eclipse.jdt.core.compiler.IProblem;
import processing.app.Problem;
/**
* Wrapper class for IProblem that stores the tabIndex and line number
* according to its tab, including the original IProblem object
*/
public class JavaProblem implements Problem {
/**
* The tab number to which the error belongs to
*/
private int tabIndex;
/**
* Line number(pde code) of the error
*/
private int lineNumber;
private int startOffset;
private int stopOffset;
/**
* Error Message. Processed form of IProblem.getMessage()
*/
private String message;
/**
* The type of error - WARNING or ERROR.
*/
private int type;
/**
* If the error is a 'cannot find type' contains the list of suggested imports
*/
private String[] importSuggestions;
public static final int ERROR = 1, WARNING = 2;
public JavaProblem(String message, int type, int tabIndex, int lineNumber) {
this.message = message;
this.type = type;
this.tabIndex = tabIndex;
this.lineNumber = lineNumber;
}
/**
*
* @param iProblem - The IProblem which is being wrapped
* @param tabIndex - The tab number to which the error belongs to
* @param lineNumber - Line number(pde code) of the error
* @param badCode - The code iProblem refers to.
*/
public static JavaProblem fromIProblem(IProblem iProblem,
int tabIndex, int lineNumber, String badCode) {
int type = 0;
if(iProblem.isError()) {
type = ERROR;
} else if (iProblem.isWarning()) {
type = WARNING;
}
String message = ErrorMessageSimplifier.getSimplifiedErrorMessage(iProblem, badCode);
return new JavaProblem(message, type, tabIndex, lineNumber);
}
public void setPDEOffsets(int startOffset, int stopOffset){
this.startOffset = startOffset;
this.stopOffset = stopOffset;
}
@Override
public int getStartOffset() {
return startOffset;
}
@Override
public int getStopOffset() {
return stopOffset;
}
@Override
public boolean isError() {
return type == ERROR;
}
@Override
public boolean isWarning() {
return type == WARNING;
}
@Override
public String getMessage() {
return message;
}
@Override
public int getTabIndex() {
return tabIndex;
}
@Override
public int getLineNumber() {
return lineNumber;
}
public String[] getImportSuggestions() {
return importSuggestions;
}
public void setImportSuggestions(String[] a) {
importSuggestions = a;
}
@Override
public String toString() {
return "TAB " + tabIndex + ",LN " + lineNumber + "LN START OFF: "
+ startOffset + ",LN STOP OFF: " + stopOffset + ",PROB: "
+ message;
}
}
@@ -0,0 +1,644 @@
/* -*- 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.mode.java.pdex;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.SwingWorker;
import processing.app.Messages;
import processing.app.Platform;
import processing.app.syntax.PdeTextArea;
import processing.app.syntax.TextAreaDefaults;
import processing.mode.java.JavaEditor;
import processing.mode.java.JavaInputHandler;
import processing.mode.java.JavaMode;
import processing.mode.java.tweak.ColorControlBox;
import processing.mode.java.tweak.Handle;
/**
* TextArea implementation for Java Mode. Primary differences from PdeTextArea
* are completions, suggestions, and tweak handling.
*/
public class JavaTextArea extends PdeTextArea {
private CompletionPanel suggestion;
public JavaTextArea(TextAreaDefaults defaults, JavaEditor editor) {
super(defaults, new JavaInputHandler(editor), editor);
suggestionGenerator = new CompletionGenerator();
tweakMode = false;
}
public JavaEditor getJavaEditor() {
return (JavaEditor) editor;
}
@Override
protected JavaTextAreaPainter createPainter(final TextAreaDefaults defaults) {
return new JavaTextAreaPainter(this, defaults);
}
// used by Tweak Mode
protected JavaTextAreaPainter getJavaPainter() {
return (JavaTextAreaPainter) painter;
}
/**
* Handles KeyEvents for TextArea (code completion begins from here).
* TODO Needs explanation of why this implemented with an override
* of processKeyEvent() instead of using listeners.
*/
@Override
public void processKeyEvent(KeyEvent evt) {
if (evt.getKeyCode() == KeyEvent.VK_ESCAPE) {
if (suggestion != null){
if (suggestion.isVisible()){
Messages.log("ESC key");
hideSuggestion();
evt.consume();
return;
}
}
} else if (evt.getKeyCode() == KeyEvent.VK_ENTER &&
evt.getID() == KeyEvent.KEY_PRESSED) {
if (suggestion != null &&
suggestion.isVisible() &&
suggestion.insertSelection(CompletionPanel.KEYBOARD_COMPLETION)) {
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:
Messages.log("BK Key");
break;
case KeyEvent.VK_SPACE:
if (suggestion != null) {
if (suggestion.isVisible()) {
Messages.log("Space bar, hide completion list");
suggestion.setInvisible();
}
}
break;
}
}
super.processKeyEvent(evt);
// code completion disabled if Java tabs present
if (!getJavaEditor().hasJavaTabs()) {
if (evt.getID() == KeyEvent.KEY_TYPED) {
processCompletionKeys(evt);
} else if (!Platform.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED) {
processCompletionKeys(evt);
} else if (Platform.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED) {
processControlSpace(evt);
}
}
}
// Special case for OS X, where Ctrl-Space is not detected as KEY_TYPED
// https://github.com/processing/processing/issues/2699
private void processControlSpace(final KeyEvent event) {
if (event.getKeyCode() == KeyEvent.VK_SPACE && event.isControlDown()) {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled) {
Messages.log("[KeyEvent]" + KeyEvent.getKeyText(event.getKeyCode()) + " |Prediction started");
fetchPhrase();
}
}
}
private void processCompletionKeys(final KeyEvent event) {
char keyChar = event.getKeyChar();
int keyCode = event.getKeyCode();
if (keyChar == KeyEvent.VK_ENTER ||
keyChar == KeyEvent.VK_ESCAPE ||
keyChar == KeyEvent.VK_TAB ||
(event.getID() == KeyEvent.KEY_RELEASED &&
keyCode != KeyEvent.VK_LEFT && keyCode != KeyEvent.VK_RIGHT)) {
// ignore
} else if (keyChar == ')') {
// https://github.com/processing/processing/issues/2741
hideSuggestion();
} else if (keyChar == '.') {
if (JavaMode.codeCompletionsEnabled) {
Messages.log("[KeyEvent]" + KeyEvent.getKeyText(event.getKeyCode()) + " |Prediction started");
fetchPhrase();
}
} else if (keyChar == ' ') { // Trigger on Ctrl-Space
if (!Platform.isMacOS() && JavaMode.codeCompletionsEnabled &&
(event.isControlDown() || event.isMetaDown())) {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled) {
// Removed for https://github.com/processing/processing/issues/3847
//try {
// getDocument().remove(getCaretPosition() - 1, 1); // Remove the typed space
Messages.log("[KeyEvent]" + event.getKeyChar() + " |Prediction started");
fetchPhrase();
//} catch (BadLocationException e) {
// e.printStackTrace();
//}
}
} else {
hideSuggestion(); // hide on spacebar
}
} else {
if (JavaMode.codeCompletionsEnabled) {
prepareSuggestions(event);
}
}
}
/** Kickstart auto-complete suggestions */
private void prepareSuggestions(final KeyEvent evt) {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled &&
(JavaMode.ccTriggerEnabled ||
(suggestion != null && suggestion.isVisible()))) {
Messages.log("[KeyEvent]" + evt.getKeyChar() + " |Prediction started");
fetchPhrase();
}
}
CompletionGenerator suggestionGenerator;
SwingWorker<Void, Void> suggestionWorker = null;
volatile boolean suggestionRunning = false;
volatile boolean suggestionRequested = false;
/**
* Retrieves the current word typed just before the caret.
* Then triggers code completion for that word.
* @param evt - the KeyEvent which triggered this method
*/
protected void fetchPhrase() {
if (suggestionRunning) {
suggestionRequested = true;
return;
}
suggestionRunning = true;
suggestionRequested = false;
final String text;
final int caretLineIndex;
final int caretLinePosition;
{
// Get caret position
int caretPosition = getCaretPosition();
if (caretPosition < 0) {
suggestionRunning = false;
return;
}
// Get line index
caretLineIndex = getCaretLine();
if (caretLineIndex < 0) {
suggestionRunning = false;
return;
}
// Get text of the line
String lineText = getLineText(caretLineIndex);
if (lineText == null) {
suggestionRunning = false;
return;
}
// Get caret position on the line
caretLinePosition = getCaretPosition() - getLineStartOffset(caretLineIndex);
if (caretLinePosition <= 0) {
suggestionRunning = false;
return;
}
// Get part of the line to the left of the caret
if (caretLinePosition > lineText.length()) {
suggestionRunning = false;
return;
}
text = lineText.substring(0, caretLinePosition);
}
// Adjust line number for tabbed sketches
//int codeIndex = editor.getSketch().getCodeIndex(getJavaEditor().getCurrentTab());
int codeIndex = editor.getSketch().getCurrentCodeIndex();
int lineStartOffset = editor.getTextArea().getLineStartOffset(caretLineIndex);
getJavaEditor().getPreprocessingService().whenDone(ps -> {
int lineNumber = ps.tabOffsetToJavaLine(codeIndex, lineStartOffset);
String phrase = null;
DefaultListModel<CompletionCandidate> defListModel = null;
try {
Messages.log("phrase parse start");
phrase = parsePhrase(text);
Messages.log("phrase: " + phrase);
if (phrase != null) {
List<CompletionCandidate> candidates;
candidates = suggestionGenerator.preparePredictions(ps, phrase, lineNumber);
if (!suggestionRequested) {
// // don't show completions when the outline is visible
// boolean showSuggestions =
// astGenerator.sketchOutline == null || !astGenerator.sketchOutline.isVisible();
// if (showSuggestions && phrase != null &&
if (candidates != null && !candidates.isEmpty()) {
Collections.sort(candidates);
defListModel = CompletionGenerator.filterPredictions(candidates);
Messages.log("Got: " + candidates.size() + " candidates, " + defListModel.size() + " filtered");
}
}
}
final String finalPhrase = phrase;
final DefaultListModel<CompletionCandidate> finalDefListModel = defListModel;
EventQueue.invokeLater(() -> {
suggestionRunning = false;
if (suggestionRequested) {
Messages.log("completion invalidated");
fetchPhrase();
return;
}
Messages.log("completion finishing");
if (finalDefListModel != null) {
showSuggestion(finalDefListModel, finalPhrase);
} else {
hideSuggestion();
}
});
} catch (Exception e) {
Messages.loge("error while preparing suggestions", e);
}
});
}
protected static String parsePhrase(final String lineText) {
boolean overloading = false;
{ // Check if we can provide suggestions for this phrase ending
String trimmedLineText = lineText.trim();
if (trimmedLineText.length() == 0) return null;
char lastChar = trimmedLineText.charAt(trimmedLineText.length() - 1);
if (lastChar == '.') {
trimmedLineText = trimmedLineText.substring(0, trimmedLineText.length() - 1).trim();
if (trimmedLineText.length() == 0) return null;
lastChar = trimmedLineText.charAt(trimmedLineText.length() - 1);
switch (lastChar) {
case ')':
case ']':
case '"':
break; // We can suggest for these
default:
if (!Character.isJavaIdentifierPart(lastChar)) {
return null; // Not something we can suggest
}
break;
}
} else if (lastChar == '(') {
overloading = true; // We can suggest overloaded methods
} else if (!Character.isJavaIdentifierPart(lastChar)) {
return null; // Not something we can suggest
}
}
final int currentCharIndex = lineText.length() - 1;
{ // Check if the caret is in the comment
int commentStart = lineText.indexOf("//", 0);
if (commentStart >= 0 && currentCharIndex > commentStart) {
return null;
}
}
// Index the line
BitSet isInLiteral = new BitSet(lineText.length());
BitSet isInBrackets = new BitSet(lineText.length());
{ // Mark parts in literals
boolean inString = false;
boolean inChar = false;
boolean inEscaped = false;
for (int i = 0; i < lineText.length(); i++) {
if (!inEscaped) {
switch (lineText.charAt(i)) {
case '\"':
if (!inChar) inString = !inString;
break;
case '\'':
if (!inString) inChar = !inChar;
break;
case '\\':
if (inString || inChar) {
inEscaped = true;
}
break;
}
} else {
inEscaped = false;
}
isInLiteral.set(i, inString || inChar);
}
}
if (isInLiteral.get(currentCharIndex)) return null;
{ // Mark parts in top level brackets
int depth = overloading ? 1 : 0;
int bracketStart = overloading ? lineText.length() : 0;
int squareDepth = 0;
int squareBracketStart = 0;
bracketLoop: for (int i = lineText.length() - 1; i >= 0; i--) {
if (!isInLiteral.get(i)) {
switch (lineText.charAt(i)) {
case ')':
if (depth == 0) bracketStart = i;
depth++;
break;
case '(':
depth--;
if (depth == 0) {
isInBrackets.set(i, bracketStart);
} else if (depth < 0) {
break bracketLoop;
}
break;
case ']':
if (squareDepth == 0) squareBracketStart = i;
squareDepth++;
break;
case '[':
squareDepth--;
if (squareDepth == 0) {
isInBrackets.set(i, squareBracketStart);
} else if (squareDepth < 0) {
break bracketLoop;
}
break;
}
}
}
if (depth > 0) isInBrackets.set(0, bracketStart);
if (squareDepth > 0) isInBrackets.set(0, squareBracketStart);
}
// Walk the line from the end while it makes sense
int position = currentCharIndex;
parseLoop: while (position >= 0) {
int currChar = lineText.charAt(position);
switch (currChar) {
case '.': // Grab it
position--;
break;
case '[':
break parseLoop; // End of scope
case ']': // Grab the whole region in square brackets
position = isInBrackets.previousClearBit(position-1);
break;
case '(':
if (isInBrackets.get(position)) {
position--; // This checks for first bracket while overloading
break;
}
break parseLoop; // End of scope
case ')': // Grab the whole region in brackets
position = isInBrackets.previousClearBit(position-1);
break;
case '"': // Grab the whole literal and quit
position = isInLiteral.previousClearBit(position - 1);
break parseLoop;
default:
if (Character.isJavaIdentifierPart(currChar)) {
position--; // Grab the identifier
} else if (Character.isWhitespace(currChar)) {
position--; // Grab whitespace too
} else {
break parseLoop; // Got a char ending the phrase
}
break;
}
}
position++;
// Extract phrase
String phrase = lineText.substring(position, lineText.length()).trim();
Messages.log(phrase);
if (phrase.length() == 0 || Character.isDigit(phrase.charAt(0))) {
return null; // Can't suggest for numbers or empty phrases
}
return phrase;
}
/**
* Calculates location of caret and displays the suggestion pop-up.
*/
protected void showSuggestion(DefaultListModel<CompletionCandidate> listModel, String subWord) {
// TODO can this be ListModel instead? why is size() in DefaultListModel
// different from getSize() in ListModel (or are they, really?)
hideSuggestion();
if (listModel.size() != 0) {
int position = getCaretPosition();
try {
Point location =
new Point(offsetToX(getCaretLine(),
position - getLineStartOffset(getCaretLine())),
lineToY(getCaretLine()) + getPainter().getLineHeight());
suggestion = new CompletionPanel(this, position, subWord,
listModel, location, getJavaEditor());
requestFocusInWindow();
} catch (Exception e) {
e.printStackTrace();
}
} else {
Messages.log("TextArea: No suggestions to show.");
}
}
/** Hides suggestion popup */
public void hideSuggestion() {
if (suggestion != null) {
suggestion.setInvisible();
//log("Suggestion hidden.");
suggestion = null; // TODO: check if we dispose the window properly
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// TWEAK MODE
// save input listeners to stop/start text edit
protected ComponentListener[] baseCompListeners;
protected MouseListener[] baseMouseListeners;
protected MouseMotionListener[] baseMotionListeners;
protected KeyListener[] baseKeyListeners;
protected boolean tweakMode;
/* remove all standard interaction listeners */
public void tweakRemoveListeners() {
if (baseCompListeners == null) {
// First time in tweak mode, grab the default listeners. Moved from the
// constructor since not all listeners may have been added at that point.
baseCompListeners = painter.getComponentListeners();
baseMouseListeners = painter.getMouseListeners();
baseMotionListeners = painter.getMouseMotionListeners();
baseKeyListeners = editor.getKeyListeners();
}
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 startTweakMode() {
// ignore if we are already in interactiveMode
if (!tweakMode) {
tweakRemoveListeners();
getJavaPainter().startTweakMode();
this.editable = false;
this.caretBlinks = false;
this.setCaretVisible(false);
tweakMode = true;
}
}
public void stopTweakMode() {
// ignore if we are not in interactive mode
if (tweakMode) {
tweakRemoveListeners();
tweakRestoreBaseListeners();
getJavaPainter().stopTweakMode();
editable = true;
caretBlinks = true;
setCaretVisible(true);
tweakMode = false;
}
}
private void tweakRestoreBaseListeners() {
// add the original text-edit listeners
for (ComponentListener cl : baseCompListeners) {
painter.addComponentListener(cl);
}
for (MouseListener ml : baseMouseListeners) {
painter.addMouseListener(ml);
}
for (MouseMotionListener mml : baseMotionListeners) {
painter.addMouseMotionListener(mml);
}
for (KeyListener kl : baseKeyListeners) {
editor.addKeyListener(kl);
}
}
public void updateInterface(List<List<Handle>> handles,
List<List<ColorControlBox>> colorBoxes) {
getJavaPainter().updateTweakInterface(handles, colorBoxes);
}
}
@@ -0,0 +1,378 @@
/* -*- 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.mode.java.pdex;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.util.List;
import processing.app.SketchCode;
import processing.app.syntax.PdeTextAreaPainter;
import processing.app.syntax.TextAreaDefaults;
import processing.mode.java.JavaEditor;
import processing.mode.java.tweak.ColorControlBox;
import processing.mode.java.tweak.ColorSelector;
import processing.mode.java.tweak.Handle;
import processing.mode.java.tweak.Settings;
/**
* Customized line painter to handle tweak mode features.
*/
public class JavaTextAreaPainter extends PdeTextAreaPainter {
public JavaTextAreaPainter(final JavaTextArea textArea, TextAreaDefaults defaults) {
super(textArea, defaults);
// TweakMode code
tweakMode = false;
cursorType = Cursor.DEFAULT_CURSOR;
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// TWEAK MODE
protected int horizontalAdjustment = 0;
public boolean tweakMode = false;
public List<List<Handle>> handles;
public List<List<ColorControlBox>> colorBoxes;
public Handle mouseHandle = null;
public ColorSelector colorSelector;
int cursorType;
BufferedImage cursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
Cursor blankCursor;
// this is a temporary workaround for the CHIP, will be removed
{
Dimension cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(16, 16);
if (cursorSize.width == 0 || cursorSize.height == 0) {
blankCursor = Cursor.getDefaultCursor();
} else {
blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(0, 0), "blank cursor");
}
}
@Override
synchronized public void paint(Graphics gfx) {
super.paint(gfx);
if (tweakMode && handles != null) {
int currentTab = getCurrentCodeIndex();
// enable anti-aliasing
Graphics2D g2d = (Graphics2D)gfx;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for (Handle n : handles.get(currentTab)) {
// update n position and width, and draw it
int lineStartChar = textArea.getLineStartOffset(n.line);
int x = textArea.offsetToX(n.line, n.newStartChar - lineStartChar);
int y = textArea.lineToY(n.line) + fm.getHeight() + 1;
int end = textArea.offsetToX(n.line, n.newEndChar - lineStartChar);
n.setPos(x, y);
n.setWidth(end - x);
n.draw(g2d, n==mouseHandle);
}
// draw color boxes
for (ColorControlBox cBox: colorBoxes.get(currentTab)) {
int lineStartChar = textArea.getLineStartOffset(cBox.getLine());
int x = textArea.offsetToX(cBox.getLine(), cBox.getCharIndex() - lineStartChar);
int y = textArea.lineToY(cBox.getLine()) + fm.getDescent();
cBox.setPos(x, y+1);
cBox.draw(g2d);
}
}
}
protected void startTweakMode() {
addMouseListener(new MouseListener() {
@Override
public void mouseReleased(MouseEvent e) {
if (mouseHandle != null) {
mouseHandle.resetProgress();
mouseHandle = null;
updateCursor(e.getX(), e.getY());
repaint();
}
}
@Override
public void mousePressed(MouseEvent e) {
int currentTab = getCurrentCodeIndex();
// check for clicks on number handles
for (Handle n : handles.get(currentTab)) {
if (n.pick(e.getX(), e.getY())) {
cursorType = -1;
JavaTextAreaPainter.this.setCursor(blankCursor);
mouseHandle = n;
mouseHandle.setCenterX(e.getX());
repaint();
return;
}
}
// check for clicks on color boxes
for (ColorControlBox box : colorBoxes.get(currentTab)) {
if (box.pick(e.getX(), e.getY())) {
if (colorSelector != null) {
// we already show a color selector, close it
colorSelector.frame.dispatchEvent(new WindowEvent(colorSelector.frame, WindowEvent.WINDOW_CLOSING));
}
colorSelector = new ColorSelector(box);
colorSelector.frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
colorSelector.frame.setVisible(false);
colorSelector = null;
}
});
colorSelector.show(getLocationOnScreen().x + e.getX() + 30,
getLocationOnScreen().y + e.getY() - 130);
}
}
}
@Override
public void mouseExited(MouseEvent e) { }
@Override
public void mouseEntered(MouseEvent e) { }
@Override
public void mouseClicked(MouseEvent e) { }
});
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
updateCursor(e.getX(), e.getY());
if (!Settings.alwaysShowColorBoxes) {
showHideColorBoxes(e.getY());
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (mouseHandle != null) {
// set the current drag amount of the arrows
mouseHandle.setCurrentX(e.getX());
// update code text with the new value
updateCodeText();
if (colorSelector != null) {
colorSelector.refreshColor();
}
repaint();
}
}
});
tweakMode = true;
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
repaint();
}
protected void stopTweakMode() {
tweakMode = false;
if (colorSelector != null) {
colorSelector.hide();
WindowEvent windowEvent =
new WindowEvent(colorSelector.frame, WindowEvent.WINDOW_CLOSING);
colorSelector.frame.dispatchEvent(windowEvent);
}
setCursor(new Cursor(Cursor.TEXT_CURSOR));
repaint();
}
protected void updateTweakInterface(List<List<Handle>> handles,
List<List<ColorControlBox>> colorBoxes) {
this.handles = handles;
this.colorBoxes = colorBoxes;
updateTweakInterfacePositions();
repaint();
}
/**
* Initialize all the number changing interfaces.
* synchronize this method to prevent the execution of 'paint' in the middle.
* (don't paint while we make changes to the text of the editor)
*/
private synchronized void updateTweakInterfacePositions() {
SketchCode[] code = getEditor().getSketch().getCode();
int prevScroll = textArea.getVerticalScrollPosition();
String prevText = textArea.getText();
for (int tab=0; tab<code.length; tab++) {
String tabCode = getJavaEditor().baseCode[tab];
textArea.setText(tabCode);
for (Handle n : handles.get(tab)) {
int lineStartChar = textArea.getLineStartOffset(n.line);
int x = textArea.offsetToX(n.line, n.newStartChar - lineStartChar);
int end = textArea.offsetToX(n.line, n.newEndChar - lineStartChar);
int y = textArea.lineToY(n.line) + fm.getHeight() + 1;
n.initInterface(x, y, end-x, fm.getHeight());
}
for (ColorControlBox cBox : colorBoxes.get(tab)) {
int lineStartChar = textArea.getLineStartOffset(cBox.getLine());
int x = textArea.offsetToX(cBox.getLine(), cBox.getCharIndex() - lineStartChar);
int y = textArea.lineToY(cBox.getLine()) + fm.getDescent();
cBox.initInterface(this, x, y+1, fm.getHeight()-2, fm.getHeight()-2);
}
}
textArea.setText(prevText);
textArea.scrollTo(prevScroll, 0);
}
/**
* Take the saved code of the current tab and replace
* all numbers with their current values.
* Update TextArea with the new code.
*/
public void updateCodeText() {
int charInc = 0;
int currentTab = getCurrentCodeIndex();
SketchCode sc = getEditor().getSketch().getCode(currentTab);
String code = getJavaEditor().baseCode[currentTab];
for (Handle n : handles.get(currentTab)) {
int s = n.startChar + charInc;
int e = n.endChar + charInc;
code = replaceString(code, s, e, n.strNewValue);
n.newStartChar = n.startChar + charInc;
charInc += n.strNewValue.length() - n.strValue.length();
n.newEndChar = n.endChar + charInc;
}
replaceTextAreaCode(code);
// update also the sketch code for later
sc.setProgram(code);
}
// don't paint while we do the stuff below
private synchronized void replaceTextAreaCode(String code) {
// by default setText will scroll all the way to the end
// remember current scroll position
int scrollLine = textArea.getVerticalScrollPosition();
int scrollHor = textArea.getHorizontalScrollPosition();
textArea.setText(code);
textArea.setOrigin(scrollLine, -scrollHor);
}
static private String replaceString(String str, int start, int end, String put) {
return str.substring(0, start) + put + str.substring(end, str.length());
}
private void updateCursor(int mouseX, int mouseY) {
int currentTab = getCurrentCodeIndex();
for (Handle n : handles.get(currentTab)) {
if (n.pick(mouseX, mouseY)) {
cursorType = Cursor.W_RESIZE_CURSOR;
setCursor(new Cursor(cursorType));
return;
}
}
for (ColorControlBox colorBox : colorBoxes.get(currentTab)) {
if (colorBox.pick(mouseX, mouseY)) {
cursorType = Cursor.HAND_CURSOR;
setCursor(new Cursor(cursorType));
return;
}
}
if (cursorType == Cursor.W_RESIZE_CURSOR ||
cursorType == Cursor.HAND_CURSOR ||
cursorType == -1) {
cursorType = Cursor.DEFAULT_CURSOR;
setCursor(new Cursor(cursorType));
}
}
private void showHideColorBoxes(int y) {
// display the box if the mouse if in the same line.
// always keep the color box of the color selector.
int currentTab = getCurrentCodeIndex();
boolean change = false;
for (ColorControlBox box : colorBoxes.get(currentTab)) {
if (box.setMouseY(y)) {
change = true;
}
}
if (colorSelector != null) {
colorSelector.colorBox.visible = true;
}
if (change) {
repaint();
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
private JavaEditor getJavaEditor() {
return (JavaEditor) getEditor();
}
private int getCurrentCodeIndex() {
return getEditor().getSketch().getCurrentCodeIndex();
}
}
@@ -0,0 +1,107 @@
package processing.mode.java.pdex;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import processing.app.SketchCode;
import processing.mode.java.JavaEditor;
public class PDEX {
static private final boolean SHOW_DEBUG_TREE = false;
private boolean enabled = true;
private ErrorChecker errorChecker;
private InspectMode inspect;
private ShowUsage usage;
private Rename rename;
private DebugTree debugTree;
private PreprocessingService pps;
public PDEX(JavaEditor editor, PreprocessingService pps) {
this.pps = pps;
this.enabled = !editor.hasJavaTabs();
errorChecker = new ErrorChecker(editor, pps);
usage = new ShowUsage(editor, pps);
inspect = new InspectMode(editor, pps, usage);
rename = new Rename(editor, pps, usage);
if (SHOW_DEBUG_TREE) {
debugTree = new DebugTree(editor, pps);
}
for (SketchCode code : editor.getSketch().getCode()) {
Document document = code.getDocument();
addDocumentListener(document);
}
sketchChanged();
}
public void addDocumentListener(Document doc) {
if (doc != null) doc.addDocumentListener(sketchChangedListener);
}
final DocumentListener sketchChangedListener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
sketchChanged();
}
@Override
public void removeUpdate(DocumentEvent e) {
sketchChanged();
}
@Override
public void changedUpdate(DocumentEvent e) {
sketchChanged();
}
};
public void sketchChanged() {
errorChecker.notifySketchChanged();
pps.notifySketchChanged();
}
public void preferencesChanged() {
errorChecker.preferencesChanged();
sketchChanged();
}
public void hasJavaTabsChanged(boolean hasJavaTabs) {
enabled = !hasJavaTabs;
if (!enabled) {
usage.hide();
}
}
public void dispose() {
inspect.dispose();
errorChecker.dispose();
usage.dispose();
rename.dispose();
if (debugTree != null) {
debugTree.dispose();
}
}
public void documentChanged(Document newDoc) {
addDocumentListener(newDoc);
}
}
@@ -0,0 +1,274 @@
package processing.mode.java.pdex;
import com.google.classpath.ClassPath;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import processing.app.Sketch;
import processing.core.PApplet;
import processing.mode.java.pdex.TextTransform.OffsetMapper;
public class PreprocessedSketch {
public final Sketch sketch;
public final CompilationUnit compilationUnit;
public final String[] classPathArray;
public final ClassPath classPath;
public final URLClassLoader classLoader;
public final String[] searchClassPathArray;
public final int[] tabStartOffsets;
public final String scrubbedPdeCode;
public final String pdeCode;
public final String javaCode;
public final OffsetMapper offsetMapper;
public final boolean hasSyntaxErrors;
public final boolean hasCompilationErrors;
public final List<ImportStatement> programImports;
public final List<ImportStatement> coreAndDefaultImports;
public final List<ImportStatement> codeFolderImports;
/// JAVA -> SKETCH -----------------------------------------------------------
public static class SketchInterval {
public static final SketchInterval BEFORE_START = new SketchInterval(-1, -1, -1, -1, -1);
private SketchInterval(int tabIndex,
int startTabOffset, int stopTabOffset,
int startPdeOffset, int stopPdeOffset) {
this.tabIndex = tabIndex;
this.startTabOffset = startTabOffset;
this.stopTabOffset = stopTabOffset;
this.startPdeOffset = startPdeOffset;
this.stopPdeOffset = stopPdeOffset;
}
final int tabIndex;
final int startTabOffset;
final int stopTabOffset;
final int startPdeOffset;
final int stopPdeOffset;
}
public boolean inRange(SketchInterval interval) {
return interval != SketchInterval.BEFORE_START &&
interval.stopPdeOffset < pdeCode.length();
}
public String getPdeCode(SketchInterval si) {
if (si == SketchInterval.BEFORE_START) return "";
int stop = Math.min(si.stopPdeOffset, pdeCode.length());
int start = Math.min(si.startPdeOffset, stop);
return pdeCode.substring(start, stop);
}
public SketchInterval mapJavaToSketch(ASTNode node) {
return mapJavaToSketch(node.getStartPosition(),
node.getStartPosition() + node.getLength());
}
public SketchInterval mapJavaToSketch(IProblem iproblem) {
return mapJavaToSketch(iproblem.getSourceStart(),
iproblem.getSourceEnd() + 1); // make it exclusive
}
public SketchInterval mapJavaToSketch(int startJavaOffset, int stopJavaOffset) {
int length = stopJavaOffset - startJavaOffset;
int startPdeOffset = javaOffsetToPdeOffset(startJavaOffset);
int stopPdeOffset;
if (length == 0) {
stopPdeOffset = startPdeOffset;
} else {
stopPdeOffset = javaOffsetToPdeOffset(stopJavaOffset-1);
if (stopPdeOffset >= 0 && (stopPdeOffset > startPdeOffset || length == 1)) {
stopPdeOffset += 1;
}
}
if (startPdeOffset < 0 || stopPdeOffset < 0) {
return SketchInterval.BEFORE_START;
}
int tabIndex = pdeOffsetToTabIndex(startPdeOffset);
if (startPdeOffset >= pdeCode.length()) {
startPdeOffset = pdeCode.length() - 1;
stopPdeOffset = startPdeOffset + 1;
}
return new SketchInterval(tabIndex,
pdeOffsetToTabOffset(tabIndex, startPdeOffset),
pdeOffsetToTabOffset(tabIndex, stopPdeOffset),
startPdeOffset, stopPdeOffset);
}
private int javaOffsetToPdeOffset(int javaOffset) {
return offsetMapper.getInputOffset(javaOffset);
}
public int pdeOffsetToTabIndex(int pdeOffset) {
pdeOffset = Math.max(0, pdeOffset);
int tab = Arrays.binarySearch(tabStartOffsets, pdeOffset);
if (tab < 0) {
tab = -(tab + 1) - 1;
}
return tab;
}
public int pdeOffsetToTabOffset(int tabIndex, int pdeOffset) {
int tabStartOffset = tabStartOffsets[clipTabIndex(tabIndex)];
return pdeOffset - tabStartOffset;
}
/// SKETCH -> JAVA -----------------------------------------------------------
public int tabOffsetToJavaOffset(int tabIndex, int tabOffset) {
int tabStartOffset = tabStartOffsets[clipTabIndex(tabIndex)];
int pdeOffset = tabStartOffset + tabOffset;
return offsetMapper.getOutputOffset(pdeOffset);
}
/// LINE NUMBERS -------------------------------------------------------------
public int tabOffsetToJavaLine(int tabIndex, int tabOffset) {
int javaOffset = tabOffsetToJavaOffset(tabIndex, tabOffset);
return offsetToLine(javaCode, javaOffset);
}
public int tabOffsetToTabLine(int tabIndex, int tabOffset) {
int tabStartOffset = tabStartOffsets[clipTabIndex(tabIndex)];
return offsetToLine(pdeCode, tabStartOffset, tabStartOffset + tabOffset);
}
// TODO: optimize
private static int offsetToLine(String text, int offset) {
return offsetToLine(text, 0, offset);
}
// TODO: optimize
private static int offsetToLine(String text, int start, int offset) {
int line = 0;
while (offset >= start) {
offset = text.lastIndexOf('\n', offset-1);
line++;
}
return line - 1;
}
/// Util ---------------------------------------------------------------------
private int clipTabIndex(int tabIndex) {
return PApplet.constrain(tabIndex, 0, tabStartOffsets.length - 1);
}
/// BUILDER BUSINESS /////////////////////////////////////////////////////////
/**
* There is a lot of fields and having constructor with this many parameters
* is just not practical. Fill stuff into builder and then simply build it.
* Builder also guards against calling methods in the middle of building process.
*/
public static class Builder {
public Sketch sketch;
public CompilationUnit compilationUnit;
public String[] classPathArray;
public ClassPath classPath;
public URLClassLoader classLoader;
public String[] searchClassPathArray;
public int[] tabStartOffsets = new int[0];
public String scrubbedPdeCode;
public String pdeCode;
public String javaCode;
public OffsetMapper offsetMapper;
public boolean hasSyntaxErrors;
public boolean hasCompilationErrors;
public final List<ImportStatement> programImports = new ArrayList<>();
public final List<ImportStatement> coreAndDefaultImports = new ArrayList<>();
public final List<ImportStatement> codeFolderImports = new ArrayList<>();
public PreprocessedSketch build() {
return new PreprocessedSketch(this);
}
}
public static PreprocessedSketch empty() {
return new Builder().build();
}
private PreprocessedSketch(Builder b) {
sketch = b.sketch;
compilationUnit = b.compilationUnit;
classPathArray = b.classPathArray;
classPath = b.classPath;
classLoader = b.classLoader;
searchClassPathArray = b.searchClassPathArray;
tabStartOffsets = b.tabStartOffsets;
scrubbedPdeCode = b.scrubbedPdeCode;
pdeCode = b.pdeCode;
javaCode = b.javaCode;
offsetMapper = b.offsetMapper != null ? b.offsetMapper : OffsetMapper.EMPTY_MAPPER;
hasSyntaxErrors = b.hasSyntaxErrors;
hasCompilationErrors = b.hasCompilationErrors;
programImports = Collections.unmodifiableList(b.programImports);
coreAndDefaultImports = Collections.unmodifiableList(b.coreAndDefaultImports);
codeFolderImports = Collections.unmodifiableList(b.codeFolderImports);
}
}
@@ -0,0 +1,725 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.pdex;
import com.google.classpath.ClassPathFactory;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.swing.text.BadLocationException;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import processing.app.Library;
import processing.app.Messages;
import processing.app.Sketch;
import processing.app.SketchCode;
import processing.app.SketchException;
import processing.app.Util;
import processing.data.IntList;
import processing.data.StringList;
import processing.mode.java.JavaEditor;
import processing.mode.java.JavaMode;
import processing.mode.java.pdex.TextTransform.OffsetMapper;
import processing.mode.java.preproc.PdePreprocessor;
import processing.mode.java.preproc.PdePreprocessor.Mode;
/**
* The main error checking service
*/
public class PreprocessingService {
protected final JavaEditor editor;
protected final ASTParser parser = ASTParser.newParser(AST.JLS8);
private final ClassPathFactory classPathFactory = new ClassPathFactory();
private final Thread preprocessingThread;
private final BlockingQueue<Boolean> requestQueue = new ArrayBlockingQueue<>(1);
private final Object requestLock = new Object();
private final AtomicBoolean codeFolderChanged = new AtomicBoolean(true);
private final AtomicBoolean librariesChanged = new AtomicBoolean(true);
private volatile boolean running;
private CompletableFuture<PreprocessedSketch> preprocessingTask = new CompletableFuture<>();
private CompletableFuture<?> lastCallback =
new CompletableFuture<Object>() {{
complete(null); // initialization block
}};
private volatile boolean isEnabled = true;
public PreprocessingService(JavaEditor editor) {
this.editor = editor;
isEnabled = !editor.hasJavaTabs();
// Register listeners for first run
whenDone(this::fireListeners);
preprocessingThread = new Thread(this::mainLoop, "ECS");
preprocessingThread.start();
}
private void mainLoop() {
running = true;
PreprocessedSketch prevResult = null;
CompletableFuture<?> runningCallbacks = null;
Messages.log("PPS: Hi!");
while (running) {
try {
try {
requestQueue.take(); // blocking until requested
} catch (InterruptedException e) {
running = false;
break;
}
Messages.log("PPS: Starting");
prevResult = preprocessSketch(prevResult);
// Wait until callbacks finish before firing new wave
// If new request arrives while waiting, break out and start preprocessing
while (requestQueue.isEmpty() && runningCallbacks != null) {
try {
runningCallbacks.get(10, TimeUnit.MILLISECONDS);
runningCallbacks = null;
} catch (TimeoutException e) { }
}
synchronized (requestLock) {
if (requestQueue.isEmpty()) {
runningCallbacks = lastCallback;
Messages.log("PPS: Done");
preprocessingTask.complete(prevResult);
}
}
} catch (Exception e) {
Messages.loge("problem in preprocessor service loop", e);
}
}
Messages.log("PPS: Bye!");
}
public void dispose() {
cancel();
running = false;
preprocessingThread.interrupt();
}
public void cancel() {
requestQueue.clear();
}
public void notifySketchChanged() {
if (!isEnabled) return;
synchronized (requestLock) {
if (preprocessingTask.isDone()) {
preprocessingTask = new CompletableFuture<>();
// Register callback which executes all listeners
whenDone(this::fireListeners);
}
requestQueue.offer(Boolean.TRUE);
}
}
public void notifyLibrariesChanged() {
Messages.log("PPS: notified libraries changed");
librariesChanged.set(true);
notifySketchChanged();
}
public void notifyCodeFolderChanged() {
Messages.log("PPS: snotified code folder changed");
codeFolderChanged.set(true);
notifySketchChanged();
}
private CompletableFuture<?> registerCallback(Consumer<PreprocessedSketch> callback) {
synchronized (requestLock) {
lastCallback = preprocessingTask
// Run callback after both preprocessing task and previous callback
.thenAcceptBothAsync(lastCallback, (ps, a) -> callback.accept(ps))
// Make sure exception in callback won't cancel whole callback chain
.handleAsync((res, e) -> {
if (e != null) Messages.loge("PPS: exception in callback", e);
return res;
});
return lastCallback;
}
}
public void whenDone(Consumer<PreprocessedSketch> callback) {
if (!isEnabled) return;
registerCallback(callback);
}
public void whenDoneBlocking(Consumer<PreprocessedSketch> callback) {
if (!isEnabled) return;
try {
registerCallback(callback).get(3000, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
// Don't care
}
}
/// LISTENERS ----------------------------------------------------------------
private Set<Consumer<PreprocessedSketch>> listeners = new CopyOnWriteArraySet<>();
public void registerListener(Consumer<PreprocessedSketch> listener) {
if (listener != null) listeners.add(listener);
}
public void unregisterListener(Consumer<PreprocessedSketch> listener) {
listeners.remove(listener);
}
private void fireListeners(PreprocessedSketch ps) {
for (Consumer<PreprocessedSketch> listener : listeners) {
try {
listener.accept(ps);
} catch (Exception e) {
Messages.loge("error when firing preprocessing listener", e);
}
}
}
/// --------------------------------------------------------------------------
private PreprocessedSketch preprocessSketch(PreprocessedSketch prevResult) {
boolean firstCheck = prevResult == null;
PreprocessedSketch.Builder result = new PreprocessedSketch.Builder();
List<ImportStatement> codeFolderImports = result.codeFolderImports;
List<ImportStatement> programImports = result.programImports;
JavaMode javaMode = (JavaMode) editor.getMode();
Sketch sketch = result.sketch = editor.getSketch();
String className = sketch.getName();
StringBuilder workBuffer = new StringBuilder();
// Combine code into one buffer
IntList tabStartsList = new IntList();
for (SketchCode sc : sketch.getCode()) {
if (sc.isExtension("pde")) {
tabStartsList.append(workBuffer.length());
if (sc.getDocument() != null) {
try {
workBuffer.append(sc.getDocumentText());
} catch (BadLocationException e) {
e.printStackTrace();
}
} else {
workBuffer.append(sc.getProgram());
}
workBuffer.append('\n');
}
}
result.tabStartOffsets = tabStartsList.array();
String pdeStage = result.pdeCode = workBuffer.toString();
boolean reloadCodeFolder = firstCheck || codeFolderChanged.getAndSet(false);
boolean reloadLibraries = firstCheck || librariesChanged.getAndSet(false);
// Core and default imports
if (coreAndDefaultImports == null) {
PdePreprocessor p = editor.createPreprocessor(null);
coreAndDefaultImports = buildCoreAndDefaultImports(p);
}
result.coreAndDefaultImports.addAll(coreAndDefaultImports);
// Prepare code folder imports
if (reloadCodeFolder) {
codeFolderImports.addAll(buildCodeFolderImports(sketch));
} else {
codeFolderImports.addAll(prevResult.codeFolderImports);
}
// TODO: convert unicode escapes to chars
SourceUtils.scrubCommentsAndStrings(workBuffer);
result.scrubbedPdeCode = workBuffer.toString();
Mode sketchMode = PdePreprocessor.parseMode(workBuffer);
// Prepare transforms to convert pde code into parsable code
TextTransform toParsable = new TextTransform(pdeStage);
toParsable.addAll(SourceUtils.insertImports(coreAndDefaultImports));
toParsable.addAll(SourceUtils.insertImports(codeFolderImports));
toParsable.addAll(SourceUtils.parseProgramImports(workBuffer, programImports));
toParsable.addAll(SourceUtils.replaceTypeConstructors(workBuffer));
toParsable.addAll(SourceUtils.replaceHexLiterals(workBuffer));
toParsable.addAll(SourceUtils.wrapSketch(sketchMode, className, workBuffer.length()));
{ // Refresh sketch classloader and classpath if imports changed
if (javaRuntimeClassPath == null) {
javaRuntimeClassPath = buildJavaRuntimeClassPath();
sketchModeClassPath = buildModeClassPath(javaMode, false);
searchModeClassPath = buildModeClassPath(javaMode, true);
}
if (reloadLibraries) {
coreLibraryClassPath = buildCoreLibraryClassPath(javaMode);
}
boolean rebuildLibraryClassPath = reloadLibraries ||
checkIfImportsChanged(programImports, prevResult.programImports);
if (rebuildLibraryClassPath) {
sketchLibraryClassPath = buildSketchLibraryClassPath(javaMode, programImports);
searchLibraryClassPath = buildSearchLibraryClassPath(javaMode);
}
boolean rebuildClassPath = reloadCodeFolder || rebuildLibraryClassPath ||
prevResult.classLoader == null || prevResult.classPath == null ||
prevResult.classPathArray == null || prevResult.searchClassPathArray == null;
if (reloadCodeFolder) {
codeFolderClassPath = buildCodeFolderClassPath(sketch);
}
if (rebuildClassPath) {
{ // Sketch class path
List<String> sketchClassPath = new ArrayList<>();
sketchClassPath.addAll(javaRuntimeClassPath);
sketchClassPath.addAll(sketchModeClassPath);
sketchClassPath.addAll(sketchLibraryClassPath);
sketchClassPath.addAll(coreLibraryClassPath);
sketchClassPath.addAll(codeFolderClassPath);
String[] classPathArray = sketchClassPath.stream().toArray(String[]::new);
URL[] urlArray = Arrays.stream(classPathArray)
.map(path -> {
try {
return Paths.get(path).toUri().toURL();
} catch (MalformedURLException e) {
Messages.loge("malformed URL when preparing sketch classloader", e);
return null;
}
})
.filter(url -> url != null)
.toArray(URL[]::new);
result.classLoader = new URLClassLoader(urlArray, null);
result.classPath = classPathFactory.createFromPaths(classPathArray);
result.classPathArray = classPathArray;
}
{ // Search class path
List<String> searchClassPath = new ArrayList<>();
searchClassPath.addAll(javaRuntimeClassPath);
searchClassPath.addAll(searchModeClassPath);
searchClassPath.addAll(searchLibraryClassPath);
searchClassPath.addAll(coreLibraryClassPath);
searchClassPath.addAll(codeFolderClassPath);
result.searchClassPathArray = searchClassPath.stream().toArray(String[]::new);
}
} else {
result.classLoader = prevResult.classLoader;
result.classPath = prevResult.classPath;
result.searchClassPathArray = prevResult.searchClassPathArray;
result.classPathArray = prevResult.classPathArray;
}
}
// Transform code to parsable state
String parsableStage = toParsable.apply();
OffsetMapper parsableMapper = toParsable.getMapper();
// Create intermediate AST for advanced preprocessing
CompilationUnit parsableCU =
makeAST(parser, parsableStage.toCharArray(), COMPILER_OPTIONS);
// Prepare advanced transforms which operate on AST
TextTransform toCompilable = new TextTransform(parsableStage);
toCompilable.addAll(SourceUtils.preprocessAST(parsableCU));
// Transform code to compilable state
String compilableStage = toCompilable.apply();
OffsetMapper compilableMapper = toCompilable.getMapper();
char[] compilableStageChars = compilableStage.toCharArray();
// Create compilable AST to get syntax problems
CompilationUnit compilableCU =
makeAST(parser, compilableStageChars, COMPILER_OPTIONS);
// Get syntax problems from compilable AST
result.hasSyntaxErrors |= Arrays.stream(compilableCU.getProblems())
.anyMatch(IProblem::isError);
// Generate bindings after getting problems - avoids
// 'missing type' errors when there are syntax problems
CompilationUnit bindingsCU =
makeASTWithBindings(parser, compilableStageChars, COMPILER_OPTIONS,
className, result.classPathArray);
// Get compilation problems
List<IProblem> bindingsProblems = Arrays.asList(bindingsCU.getProblems());
result.hasCompilationErrors = bindingsProblems.stream()
.anyMatch(IProblem::isError);
// Update builder
result.offsetMapper = parsableMapper.thenMapping(compilableMapper);
result.javaCode = compilableStage;
result.compilationUnit = bindingsCU;
// Build it
return result.build();
}
/// IMPORTS -----------------------------------------------------------------
private List<ImportStatement> coreAndDefaultImports;
private static List<ImportStatement> buildCoreAndDefaultImports(PdePreprocessor p) {
List<ImportStatement> result = new ArrayList<>();
for (String imp : p.getCoreImports()) {
result.add(ImportStatement.parse(imp));
}
for (String imp : p.getDefaultImports()) {
result.add(ImportStatement.parse(imp));
}
return result;
}
private static List<ImportStatement> buildCodeFolderImports(Sketch sketch) {
if (sketch.hasCodeFolder()) {
File codeFolder = sketch.getCodeFolder();
String codeFolderClassPath = Util.contentsToClassPath(codeFolder);
StringList codeFolderPackages = Util.packageListFromClassPath(codeFolderClassPath);
return StreamSupport.stream(codeFolderPackages.spliterator(), false)
.map(ImportStatement::wholePackage)
.collect(Collectors.toList());
}
return Collections.emptyList();
}
private static boolean checkIfImportsChanged(List<ImportStatement> prevImports,
List<ImportStatement> imports) {
if (imports.size() != prevImports.size()) {
return true;
} else {
int count = imports.size();
for (int i = 0; i < count; i++) {
if (!imports.get(i).isSameAs(prevImports.get(i))) {
return true;
}
}
}
return false;
}
/// CLASSPATHS ---------------------------------------------------------------
private List<String> javaRuntimeClassPath;
private List<String> sketchModeClassPath;
private List<String> searchModeClassPath;
private List<String> coreLibraryClassPath;
private List<String> codeFolderClassPath;
private List<String> sketchLibraryClassPath;
private List<String> searchLibraryClassPath;
private static List<String> buildCodeFolderClassPath(Sketch sketch) {
StringBuilder classPath = new StringBuilder();
// Code folder
if (sketch.hasCodeFolder()) {
File codeFolder = sketch.getCodeFolder();
String codeFolderClassPath = Util.contentsToClassPath(codeFolder);
classPath.append(codeFolderClassPath);
}
return sanitizeClassPath(classPath.toString());
}
private static List<String> buildModeClassPath(JavaMode mode, boolean search) {
StringBuilder classPath = new StringBuilder();
if (search) {
String searchClassPath = mode.getSearchPath();
if (searchClassPath != null) {
classPath.append(File.pathSeparator).append(searchClassPath);
}
} else {
Library coreLibrary = mode.getCoreLibrary();
String coreClassPath = coreLibrary != null ?
coreLibrary.getClassPath() : mode.getSearchPath();
if (coreClassPath != null) {
classPath.append(File.pathSeparator).append(coreClassPath);
}
}
return sanitizeClassPath(classPath.toString());
}
private static List<String> buildCoreLibraryClassPath(JavaMode mode) {
StringBuilder classPath = new StringBuilder();
for (Library lib : mode.coreLibraries) {
classPath.append(File.pathSeparator).append(lib.getClassPath());
}
return sanitizeClassPath(classPath.toString());
}
private static List<String> buildSearchLibraryClassPath(JavaMode mode) {
StringBuilder classPath = new StringBuilder();
for (Library lib : mode.contribLibraries) {
classPath.append(File.pathSeparator).append(lib.getClassPath());
}
return sanitizeClassPath(classPath.toString());
}
static private List<String> buildSketchLibraryClassPath(JavaMode mode,
List<ImportStatement> programImports) {
StringBuilder classPath = new StringBuilder();
programImports.stream()
.map(ImportStatement::getPackageName)
.filter(pckg -> !ignorableImport(pckg))
.map(pckg -> {
try {
return mode.getLibrary(pckg);
} catch (SketchException e) {
return null;
}
})
.filter(lib -> lib != null)
.map(Library::getClassPath)
.forEach(cp -> classPath.append(File.pathSeparator).append(cp));
return sanitizeClassPath(classPath.toString());
}
static private List<String> buildJavaRuntimeClassPath() {
StringBuilder classPath = new StringBuilder();
{ // Java runtime
String rtPath = System.getProperty("java.home") +
File.separator + "lib" + File.separator + "rt.jar";
if (new File(rtPath).exists()) {
classPath.append(File.pathSeparator).append(rtPath);
} else {
rtPath = System.getProperty("java.home") + File.separator + "jre" +
File.separator + "lib" + File.separator + "rt.jar";
if (new File(rtPath).exists()) {
classPath.append(File.pathSeparator).append(rtPath);
}
}
}
{ // JavaFX runtime
String jfxrtPath = System.getProperty("java.home") +
File.separator + "lib" + File.separator + "ext" + File.separator + "jfxrt.jar";
if (new File(jfxrtPath).exists()) {
classPath.append(File.pathSeparator).append(jfxrtPath);
} else {
jfxrtPath = System.getProperty("java.home") + File.separator + "jre" +
File.separator + "lib" + File.separator + "ext" + File.separator + "jfxrt.jar";
if (new File(jfxrtPath).exists()) {
classPath.append(File.pathSeparator).append(jfxrtPath);
}
}
}
return sanitizeClassPath(classPath.toString());
}
private static List<String> sanitizeClassPath(String classPathString) {
// Make sure class path does not contain empty string (home dir)
return Arrays.stream(classPathString.split(File.pathSeparator))
.filter(p -> p != null && !p.trim().isEmpty())
.distinct()
.collect(Collectors.toList());
}
/// --------------------------------------------------------------------------
private static CompilationUnit makeAST(ASTParser parser,
char[] source,
Map<String, String> options) {
parser.setSource(source);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(options);
parser.setStatementsRecovery(true);
return (CompilationUnit) parser.createAST(null);
}
private static CompilationUnit makeASTWithBindings(ASTParser parser,
char[] source,
Map<String, String> options,
String className,
String[] classPath) {
parser.setSource(source);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(options);
parser.setStatementsRecovery(true);
parser.setUnitName(className);
parser.setEnvironment(classPath, null, null, false);
parser.setResolveBindings(true);
return (CompilationUnit) parser.createAST(null);
}
/**
* Ignore processing packages, java.*.*. etc.
*/
static private boolean ignorableImport(String packageName) {
return (packageName.startsWith("java.") ||
packageName.startsWith("javax."));
}
static private final Map<String, String> COMPILER_OPTIONS;
static {
Map<String, String> compilerOptions = new HashMap<>();
JavaCore.setComplianceOptions(JavaCore.VERSION_1_7, compilerOptions);
// See http://help.eclipse.org/mars/index.jsp?topic=%2Forg.eclipse.jdt.doc.isv%2Fguide%2Fjdt_api_options.htm&anchor=compiler
final String[] generate = {
JavaCore.COMPILER_LINE_NUMBER_ATTR,
JavaCore.COMPILER_SOURCE_FILE_ATTR
};
final String[] ignore = {
JavaCore.COMPILER_PB_UNUSED_IMPORT,
JavaCore.COMPILER_PB_MISSING_SERIAL_VERSION,
JavaCore.COMPILER_PB_RAW_TYPE_REFERENCE,
JavaCore.COMPILER_PB_REDUNDANT_TYPE_ARGUMENTS,
JavaCore.COMPILER_PB_UNCHECKED_TYPE_OPERATION
};
final String[] warn = {
JavaCore.COMPILER_PB_NO_EFFECT_ASSIGNMENT,
JavaCore.COMPILER_PB_NULL_REFERENCE,
JavaCore.COMPILER_PB_POTENTIAL_NULL_REFERENCE,
JavaCore.COMPILER_PB_REDUNDANT_NULL_CHECK,
JavaCore.COMPILER_PB_POSSIBLE_ACCIDENTAL_BOOLEAN_ASSIGNMENT,
JavaCore.COMPILER_PB_UNUSED_LABEL,
JavaCore.COMPILER_PB_UNUSED_LOCAL,
JavaCore.COMPILER_PB_UNUSED_OBJECT_ALLOCATION,
JavaCore.COMPILER_PB_UNUSED_PARAMETER,
JavaCore.COMPILER_PB_UNUSED_PRIVATE_MEMBER
};
for (String s : generate) compilerOptions.put(s, JavaCore.GENERATE);
for (String s : ignore) compilerOptions.put(s, JavaCore.IGNORE);
for (String s : warn) compilerOptions.put(s, JavaCore.WARNING);
COMPILER_OPTIONS = Collections.unmodifiableMap(compilerOptions);
}
public void handleHasJavaTabsChange(boolean hasJavaTabs) {
isEnabled = !hasJavaTabs;
if (isEnabled) {
notifySketchChanged();
} else {
preprocessingTask.cancel(false);
}
}
}
@@ -0,0 +1,335 @@
package processing.mode.java.pdex;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JRootPane;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import javax.swing.text.BadLocationException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import processing.app.Language;
import processing.app.Platform;
import processing.app.Sketch;
import processing.app.SketchCode;
import processing.app.syntax.SyntaxDocument;
import processing.app.ui.EditorStatus;
import processing.app.ui.Toolkit;
import processing.mode.java.JavaEditor;
import processing.mode.java.pdex.PreprocessedSketch.SketchInterval;
import static processing.mode.java.pdex.ASTUtils.findAllOccurrences;
import static processing.mode.java.pdex.ASTUtils.getSimpleNameAt;
import static processing.mode.java.pdex.ASTUtils.resolveBinding;
class Rename {
final JavaEditor editor;
final PreprocessingService pps;
final ShowUsage showUsage;
final JDialog window;
final JTextField textField;
final JLabel oldNameLabel;
IBinding binding;
PreprocessedSketch ps;
Rename(JavaEditor editor, PreprocessingService pps, ShowUsage showUsage) {
this.editor = editor;
this.pps = pps;
this.showUsage = showUsage;
// Add rename option
JMenuItem renameItem = new JMenuItem(Language.text("editor.popup.rename"));
renameItem.addActionListener(e -> handleRename());
editor.getTextArea().getRightClickPopup().add(renameItem);
window = new JDialog(editor);
JRootPane rootPane = window.getRootPane();
window.setTitle("Rename");
window.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
Toolkit.registerWindowCloseKeys(rootPane, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
window.setVisible(false);
}
});
Toolkit.setIcon(window);
window.setModal(true);
window.setResizable(false);
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
binding = null;
ps = null;
}
});
//window.setSize(Toolkit.zoom(250, 130));
//window.setLayout(new BoxLayout(window.getContentPane(), BoxLayout.Y_AXIS));
Box windowBox = Box.createVerticalBox();
Toolkit.setBorder(windowBox);
final int GAP = Toolkit.zoom(5);
{ // old name
Box oldBox = Box.createHorizontalBox();
oldNameLabel = new JLabel("Current Name: ");
oldBox.add(oldNameLabel);
//oldBox.add(Box.createHorizontalStrut(10));
oldBox.add(Box.createHorizontalGlue());
windowBox.add(oldBox);
windowBox.add(Box.createVerticalStrut(GAP));
}
{ // new name
Box newBox = Box.createHorizontalBox();
JLabel newNameLabel = new JLabel("New Name: ");
newBox.add(newNameLabel);
newBox.add(textField = new JTextField(20));
newBox.add(Box.createHorizontalGlue());
windowBox.add(newBox);
windowBox.add(Box.createVerticalStrut(GAP*2));
}
{ // button panel
JButton showUsageButton = new JButton("Show Usage");
showUsageButton.addActionListener(e -> {
showUsage.findUsageAndUpdateTree(ps, binding);
window.setVisible(false);
});
JButton renameButton = new JButton("Rename");
renameButton.addActionListener(e -> {
final String newName = textField.getText().trim();
if (!newName.isEmpty()) {
if (newName.length() >= 1 &&
newName.chars().limit(1).allMatch(Character::isUnicodeIdentifierStart) &&
newName.chars().skip(1).allMatch(Character::isUnicodeIdentifierPart)) {
rename(ps, binding, newName);
window.setVisible(false);
} else {
String msg = String.format("'%s' is not a valid name", newName);
JOptionPane.showMessageDialog(editor, msg, "Naming is Hard",
JOptionPane.PLAIN_MESSAGE);
}
}
});
rootPane.setDefaultButton(renameButton);
//JPanel panelBottom = new JPanel();
//panelBottom.setLayout(new BoxLayout(panelBottom, BoxLayout.X_AXIS));
Box buttonBox = Box.createHorizontalBox();
//Toolkit.setBorder(panelBottom, 5, 5, 5, 5);
buttonBox.add(Box.createHorizontalGlue());
buttonBox.add(showUsageButton);
if (!Platform.isMacOS()) {
buttonBox.add(Box.createHorizontalStrut(GAP));
}
buttonBox.add(renameButton);
buttonBox.add(Box.createHorizontalGlue());
Dimension showDim = showUsageButton.getPreferredSize();
Dimension renameDim = renameButton.getPreferredSize();
final int niceSize = Math.max(showDim.width, renameDim.width) + GAP;
final Dimension buttonDim = new Dimension(niceSize, showDim.height);
showUsageButton.setPreferredSize(buttonDim);
renameButton.setPreferredSize(buttonDim);
windowBox.add(buttonBox);
//window.add(panelBottom);
}
window.add(windowBox);
window.pack();
//window.setMinimumSize(window.getSize());
}
// Thread: EDT
void handleRename() {
int startOffset = editor.getSelectionStart();
int stopOffset = editor.getSelectionStop();
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleRename(ps, tabIndex, startOffset, stopOffset));
}
// Thread: worker
void handleRename(PreprocessedSketch ps, int tabIndex, int startTabOffset, int stopTabOffset) {
if (ps.hasSyntaxErrors) {
editor.statusMessage("Cannot rename until syntax errors are fixed",
EditorStatus.WARNING);
return;
}
ASTNode root = ps.compilationUnit;
// Map offsets
int startJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, startTabOffset);
int stopJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, stopTabOffset);
// Find the node
SimpleName name = getSimpleNameAt(root, startJavaOffset, stopJavaOffset);
if (name == null) {
editor.statusMessage("Highlight the class/function/variable name first",
EditorStatus.NOTICE);
return;
}
// Find binding
IBinding binding = resolveBinding(name);
if (binding == null) {
editor.statusMessage(name.getIdentifier() + " isn't defined in this sketch, " +
"so it cannot be renamed", EditorStatus.ERROR);
return;
}
ASTNode decl = ps.compilationUnit.findDeclaringNode(binding.getKey());
if (decl == null) {
editor.statusMessage(name.getIdentifier() + " isn't defined in this sketch, " +
"so it cannot be renamed", EditorStatus.ERROR);
return;
}
// Display the rename dialog
EventQueue.invokeLater(() -> {
if (!window.isVisible()) {
this.ps = ps;
this.binding = binding;
oldNameLabel.setText("Current name: " + binding.getName());
textField.setText(binding.getName());
textField.requestFocus();
textField.selectAll();
int x = editor.getX() + (editor.getWidth() - window.getWidth()) / 2;
int y = editor.getY() + (editor.getHeight() - window.getHeight()) / 2;
window.setLocation(x, y);
window.setVisible(true);
window.toFront();
}
});
}
// Thread: EDT (we can't allow user to mess with sketch while renaming)
void rename(PreprocessedSketch ps, IBinding binding, String newName) {
CompilationUnit root = ps.compilationUnit;
// Renaming constructor should rename class
if (binding.getKind() == IBinding.METHOD) {
IMethodBinding method = (IMethodBinding) binding;
if (method.isConstructor()) {
binding = method.getDeclaringClass();
}
}
ASTNode decl = root.findDeclaringNode(binding.getKey());
if (decl == null) return;
showUsage.hide();
List<SimpleName> occurrences = new ArrayList<>();
occurrences.addAll(findAllOccurrences(root, binding.getKey()));
// Renaming class should rename all constructors
if (binding.getKind() == IBinding.TYPE) {
ITypeBinding type = (ITypeBinding) binding;
//type = type.getErasure();
IMethodBinding[] methods = type.getDeclaredMethods();
Arrays.stream(methods)
.filter(IMethodBinding::isConstructor)
.flatMap(c -> findAllOccurrences(root, c.getKey()).stream())
.forEach(occurrences::add);
}
Map<Integer, List<SketchInterval>> mappedNodes = occurrences.stream()
.map(ps::mapJavaToSketch)
.filter(ps::inRange)
.collect(Collectors.groupingBy(interval -> interval.tabIndex));
Sketch sketch = ps.sketch;
editor.startCompoundEdit();
mappedNodes.entrySet().forEach(entry -> {
int tabIndex = entry.getKey();
SketchCode sketchCode = sketch.getCode(tabIndex);
SyntaxDocument document = (SyntaxDocument) sketchCode.getDocument();
List<SketchInterval> nodes = entry.getValue();
nodes.stream()
// Replace from the end so all unprocess offsets stay valid
.sorted(Comparator.comparing((SketchInterval si) -> si.startTabOffset).reversed())
.forEach(si -> {
// Make sure offsets are in bounds
int documentLength = document.getLength();
if (si.startTabOffset >= 0 && si.startTabOffset <= documentLength &&
si.stopTabOffset >= 0 && si.stopTabOffset <= documentLength) {
// Replace the code
int length = si.stopTabOffset - si.startTabOffset;
try {
document.remove(si.startTabOffset, length);
document.insertString(si.startTabOffset, newName, null);
} catch (BadLocationException e) { /* Whatever */ }
}
});
try {
sketchCode.setProgram(document.getText(0, document.getLength()));
} catch (BadLocationException e) { /* Whatever */ }
sketchCode.setModified(true);
});
editor.stopCompoundEdit();
editor.repaintHeader();
int currentTabIndex = sketch.getCurrentCodeIndex();
final int currentOffset = editor.getCaretOffset();
int precedingIntervals =
(int) mappedNodes.getOrDefault(currentTabIndex, Collections.emptyList())
.stream()
.filter(interval -> interval.stopTabOffset < currentOffset)
.count();
int intervalLengthDiff = newName.length() - binding.getName().length();
int offsetDiff = precedingIntervals * intervalLengthDiff;
editor.getTextArea().setCaretPosition(currentOffset + offsetDiff);
}
void dispose() {
if (window != null) {
window.dispose();
}
}
}
@@ -0,0 +1,329 @@
package processing.mode.java.pdex;
import static processing.mode.java.pdex.ASTUtils.findAllOccurrences;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.swing.JDialog;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.WindowConstants;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import processing.app.Language;
import processing.app.ui.EditorStatus;
import processing.app.ui.Toolkit;
import processing.app.ui.ZoomTreeCellRenderer;
import processing.mode.java.JavaEditor;
import processing.mode.java.pdex.PreprocessedSketch.SketchInterval;
class ShowUsage {
final JDialog window;
final JTree tree;
final JavaEditor editor;
final PreprocessingService pps;
final Consumer<PreprocessedSketch> reloadListener;
IBinding binding;
ShowUsage(JavaEditor editor, PreprocessingService pps) {
this.editor = editor;
this.pps = pps;
// Add show usage option
JMenuItem showUsageItem =
new JMenuItem(Language.text("editor.popup.show_usage"));
showUsageItem.addActionListener(e -> handleShowUsage());
editor.getTextArea().getRightClickPopup().add(showUsageItem);
reloadListener = this::reloadShowUsage;
{ // Show Usage window
window = new JDialog(editor);
window.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
window.setAutoRequestFocus(false);
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
binding = null;
tree.setModel(null);
pps.unregisterListener(reloadListener);
}
@Override
public void componentShown(ComponentEvent e) {
pps.registerListener(reloadListener);
}
});
window.setSize(Toolkit.zoom(300, 400));
window.setFocusableWindowState(false);
Toolkit.setIcon(window);
JScrollPane sp2 = new JScrollPane();
tree = new JTree();
ZoomTreeCellRenderer renderer =
new ZoomTreeCellRenderer(editor.getMode());
tree.setCellRenderer(renderer);
renderer.setLeafIcon(null);
renderer.setClosedIcon(null);
renderer.setOpenIcon(null);
renderer.setBackgroundSelectionColor(new Color(228, 248, 246));
renderer.setBorderSelectionColor(new Color(0, 0, 0, 0));
renderer.setTextSelectionColor(Color.BLACK);
sp2.setViewportView(tree);
window.add(sp2);
}
tree.addTreeSelectionListener(e -> {
if (tree.getLastSelectedPathComponent() != null) {
DefaultMutableTreeNode tnode =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (tnode.getUserObject() instanceof ShowUsageTreeNode) {
ShowUsageTreeNode node = (ShowUsageTreeNode) tnode.getUserObject();
editor.highlight(node.tabIndex, node.startTabOffset, node.stopTabOffset);
}
}
});
}
// Thread: EDT
void handleShowUsage() {
int startOffset = editor.getSelectionStart();
int stopOffset = editor.getSelectionStop();
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleShowUsage(ps, tabIndex, startOffset, stopOffset));
}
// Thread: worker
void handleShowUsage(PreprocessedSketch ps, int tabIndex,
int startTabOffset, int stopTabOffset) {
// Map offsets
int startJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, startTabOffset);
int stopJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, stopTabOffset);
// Find the node
SimpleName name = ASTUtils.getSimpleNameAt(ps.compilationUnit, startJavaOffset, stopJavaOffset);
if (name == null) {
editor.statusMessage("Cannot find any name under cursor", EditorStatus.NOTICE);
return;
}
// Find binding
IBinding binding = ASTUtils.resolveBinding(name);
if (binding == null) {
editor.statusMessage("Cannot find usages, try to fix errors in your code first",
EditorStatus.NOTICE);
return;
}
findUsageAndUpdateTree(ps, binding);
}
// Thread: worker
void findUsageAndUpdateTree(PreprocessedSketch ps, IBinding binding) {
this.binding = binding;
// Get label
String bindingType = "";
switch (binding.getKind()) {
case IBinding.METHOD:
IMethodBinding method = (IMethodBinding) binding;
if (method.isConstructor()) bindingType = "Constructor";
else bindingType = "Method";
break;
case IBinding.TYPE:
bindingType = "Type";
break;
case IBinding.VARIABLE:
IVariableBinding variable = (IVariableBinding) binding;
if (variable.isField()) bindingType = "Field";
else if (variable.isParameter()) bindingType = "Parameter";
else if (variable.isEnumConstant()) bindingType = "Enum constant";
else bindingType = "Local variable";
break;
}
// Find usages, map to tree nodes, add to root node
String bindingKey = binding.getKey();
List<SketchInterval> intervals =
findAllOccurrences(ps.compilationUnit, bindingKey).stream()
.map(ps::mapJavaToSketch)
// remove occurrences which fall into generated header
.filter(ps::inRange)
// remove empty intervals (happens when occurence was inserted)
.filter(in -> in.startPdeOffset < in.stopPdeOffset)
.collect(Collectors.toList());
int usageCount = intervals.size();
// Get element name from PDE code if possible, otherwise use one from Java
String elementName = intervals.stream()
.findAny()
.map(si -> ps.pdeCode.substring(si.startPdeOffset, si.stopPdeOffset))
.orElseGet(binding::getName);
// Create root node
DefaultMutableTreeNode rootNode =
new DefaultMutableTreeNode(bindingType + ": " + elementName);
intervals.stream()
// Convert to TreeNodes
.map(in -> ShowUsageTreeNode.fromSketchInterval(ps, in))
// Group by tab index
.collect(Collectors.groupingBy(node -> node.tabIndex))
// Stream Map Entries of (tab index) <-> (List<ShowUsageTreeNode>)
.entrySet().stream()
// Sort by tab index
.sorted(Comparator.comparing(Map.Entry::getKey))
.map(entry -> {
Integer tabIndex = entry.getKey();
List<ShowUsageTreeNode> nodes = entry.getValue();
int count = nodes.size();
String usageLabel = count == 1 ? "usage" : "usages";
// Create new DefaultMutableTreeNode for this tab
String tabLabel = "<html><font color=#222222>" +
ps.sketch.getCode(tabIndex).getPrettyName() +
"</font> <font color=#999999>" + count + " " + usageLabel + "</font></html>";
DefaultMutableTreeNode tabNode = new DefaultMutableTreeNode(tabLabel);
// Stream nodes belonging to this tab
nodes.stream()
// Convert TreeNodes to DefaultMutableTreeNodes
.map(DefaultMutableTreeNode::new)
// Add all as children of tab node
.forEach(tabNode::add);
return tabNode;
})
// Add all tab nodes as children of root node
.forEach(rootNode::add);
TreeModel treeModel = new DefaultTreeModel(rootNode);
// Update tree
EventQueue.invokeLater(() -> {
tree.setModel(treeModel);
// Expand all nodes
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}
tree.setRootVisible(true);
if (!window.isVisible()) {
window.setVisible(true);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice defaultScreen = ge.getDefaultScreenDevice();
Rectangle rect = defaultScreen.getDefaultConfiguration().getBounds();
int maxX = (int) rect.getMaxX() - window.getWidth();
int x = Math.min(editor.getX() + editor.getWidth(), maxX);
int y = (x == maxX) ? 10 : editor.getY();
window.setLocation(x, y);
}
window.toFront();
window.setTitle("Usage of \"" + elementName + "\" : " +
usageCount + " time(s)");
});
}
// Thread: worker
void reloadShowUsage(PreprocessedSketch ps) {
if (binding != null) {
findUsageAndUpdateTree(ps, binding);
}
}
void hide() {
window.setVisible(false);
}
void dispose() {
if (window != null) {
window.dispose();
}
}
}
class ShowUsageTreeNode {
final int tabIndex;
final int startTabOffset;
final int stopTabOffset;
final String text;
ShowUsageTreeNode(int tabIndex, int startTabOffset, int stopTabOffset, String text) {
this.tabIndex = tabIndex;
this.startTabOffset = startTabOffset;
this.stopTabOffset = stopTabOffset;
this.text = text;
}
static ShowUsageTreeNode fromSketchInterval(PreprocessedSketch ps, SketchInterval in) {
int lineStartPdeOffset = ps.pdeCode.lastIndexOf('\n', in.startPdeOffset) + 1;
int lineStopPdeOffset = ps.pdeCode.indexOf('\n', in.stopPdeOffset);
if (lineStopPdeOffset == -1) lineStopPdeOffset = ps.pdeCode.length();
int highlightStartOffset = in.startPdeOffset - lineStartPdeOffset;
int highlightStopOffset = in.stopPdeOffset - lineStartPdeOffset;
int tabLine = ps.tabOffsetToTabLine(in.tabIndex, in.startTabOffset);
// TODO: what a mess
String line = ps.pdeCode.substring(lineStartPdeOffset, lineStopPdeOffset);
String pre = line.substring(0, highlightStartOffset)
.replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;");
String highlight = line.substring(highlightStartOffset, highlightStopOffset)
.replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;");
String post = line.substring(highlightStopOffset)
.replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;");
line = pre + "<font color=#222222><b>" + highlight + "</b></font>" + post;
line = line.trim();
String text = "<html><font color=#bbbbbb>" +
(tabLine + 1) + "</font> <font color=#777777>" + line + "</font></html>";
return new ShowUsageTreeNode(in.tabIndex, in.startTabOffset, in.stopTabOffset, text);
}
@Override
public String toString() {
return text;
}
}
@@ -0,0 +1,371 @@
package processing.mode.java.pdex;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.SimpleType;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import processing.mode.java.pdex.TextTransform.Edit;
import processing.mode.java.preproc.PdePreprocessor;
public class SourceUtils {
public static final Pattern IMPORT_REGEX =
Pattern.compile("(?:^|;)\\s*(import\\s+(?:(static)\\s+)?((?:\\w+\\s*\\.)*)\\s*(\\S+)\\s*;)",
Pattern.MULTILINE | Pattern.DOTALL);
public static final Pattern IMPORT_REGEX_NO_KEYWORD =
Pattern.compile("^\\s*((?:(static)\\s+)?((?:\\w+\\s*\\.)*)\\s*(\\S+))",
Pattern.MULTILINE | Pattern.DOTALL);
public static List<ImportStatement> parseProgramImports(CharSequence source) {
List<ImportStatement> result = new ArrayList<>();
Matcher matcher = IMPORT_REGEX.matcher(source);
while (matcher.find()) {
ImportStatement is = ImportStatement.parse(matcher.toMatchResult());
result.add(is);
}
return result;
}
public static List<Edit> parseProgramImports(CharSequence source,
List<ImportStatement> outImports) {
List<Edit> result = new ArrayList<>();
Matcher matcher = IMPORT_REGEX.matcher(source);
while (matcher.find()) {
ImportStatement is = ImportStatement.parse(matcher.toMatchResult());
outImports.add(is);
int idx = matcher.start(1);
int len = matcher.end(1) - idx;
// Remove the import from the main program
// Substitute with white spaces
result.add(Edit.move(idx, len, 0));
result.add(Edit.insert(0, "\n"));
}
return result;
}
// Positive lookahead and lookbehind are needed to match all type constructors
// in code like `int(byte(245))` where first bracket matches as last
// group in "^int(" but also as a first group in "(byte(". Lookahead and
// lookbehind won't consume the shared character.
public static final Pattern TYPE_CONSTRUCTOR_REGEX =
Pattern.compile("(?<=^|\\W)(int|char|float|boolean|byte)(?=\\s*\\()",
Pattern.MULTILINE);
public static List<Edit> replaceTypeConstructors(CharSequence source) {
List<Edit> result = new ArrayList<>();
Matcher matcher = TYPE_CONSTRUCTOR_REGEX.matcher(source);
while (matcher.find()) {
String match = matcher.group(1);
int offset = matcher.start(1);
int length = match.length();
result.add(Edit.insert(offset, "PApplet."));
String replace = "parse"
+ Character.toUpperCase(match.charAt(0)) + match.substring(1);
result.add(Edit.replace(offset, length, replace));
}
return result;
}
public static final Pattern HEX_LITERAL_REGEX =
Pattern.compile("(?<=^|\\W)(#[A-Fa-f0-9]{6})(?=\\W|$)");
public static List<Edit> replaceHexLiterals(CharSequence source) {
// Find all #[webcolor] and replace with 0xff[webcolor]
// Should be 6 digits only.
List<Edit> result = new ArrayList<>();
Matcher matcher = HEX_LITERAL_REGEX.matcher(source);
while (matcher.find()) {
int offset = matcher.start(1);
result.add(Edit.replace(offset, 1, "0xff"));
}
return result;
}
public static List<Edit> insertImports(List<ImportStatement> imports) {
List<Edit> result = new ArrayList<>();
for (ImportStatement imp : imports) {
result.add(Edit.insert(0, imp.getFullSourceLine() + "\n"));
}
return result;
}
public static List<Edit> wrapSketch(PdePreprocessor.Mode mode, String className, int sourceLength) {
List<Edit> edits = new ArrayList<>();
StringBuilder b = new StringBuilder();
// Header
if (mode != PdePreprocessor.Mode.JAVA) {
b.append("\npublic class ").append(className).append(" extends PApplet {\n");
if (mode == PdePreprocessor.Mode.STATIC) {
b.append("public void setup() {\n");
}
}
edits.add(Edit.insert(0, b.toString()));
// Reset builder
b.setLength(0);
// Footer
if (mode != PdePreprocessor.Mode.JAVA) {
if (mode == PdePreprocessor.Mode.STATIC) {
// no noLoop() here so it does not tell you
// "can't invoke noLoop() on obj" when you type "obj."
b.append("\n}");
}
b.append("\n}\n");
}
edits.add(Edit.insert(sourceLength, b.toString()));
return edits;
}
// Verifies that whole input String is floating point literal. Can't be used for searching.
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-DecimalFloatingPointLiteral
public static final Pattern FLOATING_POINT_LITERAL_VERIFIER;
static {
final String DIGITS = "(?:[0-9]|[0-9][0-9_]*[0-9])";
final String EXPONENT_PART = "(?:[eE][+-]?" + DIGITS + ")";
FLOATING_POINT_LITERAL_VERIFIER = Pattern.compile(
"(?:^" + DIGITS + "\\." + DIGITS + "?" + EXPONENT_PART + "?[fFdD]?$)|" +
"(?:^\\." + DIGITS + EXPONENT_PART + "?[fFdD]?$)|" +
"(?:^" + DIGITS + EXPONENT_PART + "[fFdD]?$)|" +
"(?:^" + DIGITS + EXPONENT_PART + "?[fFdD]$)");
}
// Mask to quickly resolve whether there are any access modifiers present
private static final int ACCESS_MODIFIERS_MASK =
Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED;
public static List<Edit> preprocessAST(CompilationUnit cu) {
final List<Edit> edits = new ArrayList<>();
// Walk the tree
cu.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleType node) {
// replace "color" with "int"
if ("color".equals(node.getName().toString())) {
edits.add(Edit.replace(node.getStartPosition(), node.getLength(), "int"));
}
return super.visit(node);
}
@Override
public boolean visit(NumberLiteral node) {
// add 'f' to floats
String s = node.getToken().toLowerCase();
if (FLOATING_POINT_LITERAL_VERIFIER.matcher(s).matches() && !s.endsWith("f") && !s.endsWith("d")) {
edits.add(Edit.insert(node.getStartPosition() + node.getLength(), "f"));
}
return super.visit(node);
}
@Override
public boolean visit(MethodDeclaration node) {
// add 'public' to methods with default visibility
int accessModifiers = node.getModifiers() & ACCESS_MODIFIERS_MASK;
if (accessModifiers == 0) {
edits.add(Edit.insert(node.getStartPosition(), "public "));
}
return super.visit(node);
}
});
return edits;
}
public static final Pattern COLOR_TYPE_REGEX =
Pattern.compile("(?:^|^\\p{javaJavaIdentifierPart})(color)\\s(?!\\s*\\()",
Pattern.MULTILINE | Pattern.UNICODE_CHARACTER_CLASS);
public static List<Edit> replaceColorRegex(CharSequence source) {
final List<Edit> edits = new ArrayList<>();
Matcher matcher = COLOR_TYPE_REGEX.matcher(source);
while (matcher.find()) {
int offset = matcher.start(1);
edits.add(Edit.replace(offset, 5, "int"));
}
return edits;
}
public static final Pattern NUMBER_LITERAL_REGEX =
Pattern.compile("[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
public static List<Edit> fixFloatsRegex(CharSequence source) {
final List<Edit> edits = new ArrayList<>();
Matcher matcher = NUMBER_LITERAL_REGEX.matcher(source);
while (matcher.find()) {
int offset = matcher.start();
int end = matcher.end();
String group = matcher.group().toLowerCase();
boolean isFloatingPoint = group.contains(".") || group.contains("e");
boolean hasSuffix = end < source.length() &&
Character.toLowerCase(source.charAt(end)) != 'f' &&
Character.toLowerCase(source.charAt(end)) != 'd';
if (isFloatingPoint && !hasSuffix) {
edits.add(Edit.insert(offset, "f"));
}
}
return edits;
}
static public String scrubCommentsAndStrings(String p) {
StringBuilder sb = new StringBuilder(p);
scrubCommentsAndStrings(sb);
return sb.toString();
}
static public void scrubCommentsAndStrings(StringBuilder p) {
final int length = p.length();
final int OUT = 0;
final int IN_BLOCK_COMMENT = 1;
final int IN_EOL_COMMENT = 2;
final int IN_STRING_LITERAL = 3;
final int IN_CHAR_LITERAL = 4;
int blockStart = -1;
int prevState = OUT;
int state = OUT;
for (int i = 0; i <= length; i++) {
char ch = (i < length) ? p.charAt(i) : 0;
char pch = (i == 0) ? 0 : p.charAt(i-1);
// Get rid of double backslash immediately, otherwise
// the second backslash incorrectly triggers a new escape sequence
if (pch == '\\' && ch == '\\') {
p.setCharAt(i-1, ' ');
p.setCharAt(i, ' ');
pch = ' ';
ch = ' ';
}
switch (state) {
case OUT:
switch (ch) {
case '\'': state = IN_CHAR_LITERAL; break;
case '"': state = IN_STRING_LITERAL; break;
case '*': if (pch == '/') state = IN_BLOCK_COMMENT; break;
case '/': if (pch == '/') state = IN_EOL_COMMENT; break;
}
break;
case IN_BLOCK_COMMENT:
if (pch == '*' && ch == '/' && (i - blockStart) > 0) {
state = OUT;
}
break;
case IN_EOL_COMMENT:
if (ch == '\r' || ch == '\n') {
state = OUT;
}
break;
case IN_STRING_LITERAL:
if ((pch != '\\' && ch == '"') || ch == '\r' || ch == '\n') {
state = OUT;
}
break;
case IN_CHAR_LITERAL:
if ((pch != '\\' && ch == '\'') || ch == '\r' || ch == '\n') {
state = OUT;
}
break;
}
// Terminate ongoing block at last char
if (i == length) {
state = OUT;
}
// Handle state changes
if (state != prevState) {
if (state != OUT) {
// Entering block
blockStart = i + 1;
} else {
// Exiting block
int blockEnd = i;
if (prevState == IN_BLOCK_COMMENT && i < length) blockEnd--; // preserve star in '*/'
for (int j = blockStart; j < blockEnd; j++) {
char c = p.charAt(j);
if (c != '\n' && c != '\r') p.setCharAt(j, ' ');
}
}
}
prevState = state;
}
}
// TODO: move this to a better place when JavaBuild starts using JDT and we
// don't need to check errors at two different places [jv 2017-09-19]
/**
* Checks a single code fragment (such as a tab) for non-matching braces.
* Broken out to allow easy use in JavaBuild.
* @param c Program code scrubbed of comments and string literals.
* @param start Start index, inclusive.
* @param end End index, exclusive.
* @return {@code int[4]} Depth at which the loop stopped, followed by the
* line number, column, and string index (within the range) at which
* an error was found, if any.
*/
static public int[] checkForMissingBraces(CharSequence c, int start, int end) {
int depth = 0;
int lineNumber = 0;
int lineStart = start;
for (int i = start; i < end; i++) {
char ch = c.charAt(i);
switch (ch) {
case '{':
depth++;
break;
case '}':
depth--;
break;
case '\n':
lineNumber++;
lineStart = i;
break;
}
if (depth < 0) {
return new int[] {depth, lineNumber, i - lineStart, i - start};
}
}
return new int[] {depth, lineNumber - 1, end - lineStart - 2, end - start - 2};
}
}
@@ -0,0 +1,327 @@
package processing.mode.java.pdex;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ListIterator;
import java.util.stream.Collectors;
import processing.core.PApplet;
public class TextTransform {
private static final Comparator<Edit> INPUT_OFFSET_COMP =
(o1, o2) -> Integer.compare(o1.fromOffset, o2.fromOffset);
private static final Comparator<Edit> OUTPUT_OFFSET_COMP =
(o1, o2) -> Integer.compare(o1.toOffset, o2.toOffset);
private CharSequence input;
private List<Edit> edits = new ArrayList<>();
private List<Edit> inMap = new ArrayList<>();
private List<Edit> outMap = new ArrayList<>();
private boolean built;
private int builtForLength;
TextTransform(CharSequence input) {
this.input = input;
}
public void add(Edit edit) {
edits.add(edit);
built = false;
}
public void addAll(Collection<Edit> edits) {
this.edits.addAll(edits);
built = false;
}
public String apply() {
final int inLength = input.length();
final StringBuilder output = new StringBuilder(inLength);
buildIfNeeded(inLength);
outMap.stream()
// Filter out Delete edits
.filter(outEdit -> outEdit.toLength > 0)
.forEach(outEdit -> {
if (outEdit.outputText != null) {
// Insert or Replace edit
output.append(outEdit.outputText);
} else {
// Move edit
output.append(input, outEdit.fromOffset, outEdit.fromOffset + outEdit.fromLength);
}
});
return output.toString();
}
public OffsetMapper getMapper() {
int inLength = input.length();
buildIfNeeded(inLength);
return new SimpleOffsetMapper(inMap, outMap);
}
private void buildIfNeeded(int inLength) {
if (built && inLength == builtForLength) return;
// Make copies of Edits to preserve original edits
List<Edit> inEdits = edits.stream().map(Edit::new).collect(Collectors.toList());
List<Edit> outEdits = new ArrayList<>(inEdits);
// Edits sorted by input offsets
Collections.sort(inEdits, INPUT_OFFSET_COMP);
// Edits sorted by output offsets
Collections.sort(outEdits, OUTPUT_OFFSET_COMP);
// TODO: add some validation
// Input
ListIterator<Edit> inIt = inEdits.listIterator();
Edit inEdit = inIt.hasNext() ? inIt.next() : null;
int inEditOff = inEdit == null ? inLength : inEdit.fromOffset;
// Output
ListIterator<Edit> outIt = outEdits.listIterator();
Edit outEdit = outIt.hasNext() ? outIt.next() : null;
int outEditOff = outEdit == null ? inLength : outEdit.toOffset;
int inOffset = 0;
int outOffset = 0;
inMap.clear();
outMap.clear();
// Walk through the input, apply changes, create mapping
while (inOffset < inLength || inEdit != null || outEdit != null) {
{ // Create mapping for unmodified portion of the input
int nextEditOffset = Math.min(inEditOff, outEditOff);
{ // Insert move block to have mapping for unmodified portions too
int length = nextEditOffset - inOffset;
if (length > 0) {
Edit ch = Edit.move(inOffset, length, outOffset);
inMap.add(ch);
outMap.add(ch);
}
}
// Move offsets accordingly
outOffset += nextEditOffset - inOffset;
inOffset = nextEditOffset;
}
// Process encountered input edits
while (inEdit != null && inOffset >= inEditOff) {
inOffset += inEdit.fromLength;
if (inEdit.fromLength > 0) inMap.add(inEdit);
inEdit = inIt.hasNext() ? inIt.next() : null;
inEditOff = inEdit != null ? inEdit.fromOffset : inLength;
}
// Process encountered output edits
while (outEdit != null && inOffset >= outEditOff) {
outEdit.toOffset = outOffset;
if (outEdit.toLength > 0) outMap.add(outEdit);
outOffset += outEdit.toLength;
outEdit = outIt.hasNext() ? outIt.next() : null;
outEditOff = outEdit != null ? outEdit.toOffset : inLength;
}
}
built = true;
builtForLength = inLength;
}
@Override
public String toString() {
return "SourceMapping{" +
"edits=" + edits +
'}';
}
protected static class Edit {
static Edit insert(int offset, String text) {
return new Edit(offset, 0, offset, text.length(), text);
}
static Edit replace(int offset, int length, String text) {
return new Edit(offset, length, offset, text.length(), text);
}
static Edit move(int fromOffset, int length, int toOffset) {
Edit result = new Edit(fromOffset, length, toOffset, length, null);
result.toOffset = toOffset;
return result;
}
static Edit delete(int position, int length) {
return new Edit(position, length, position, 0, null);
}
Edit(Edit edit) {
this.fromOffset = edit.fromOffset;
this.fromLength = edit.fromLength;
this.toOffset = edit.toOffset;
this.toLength = edit.toLength;
this.outputText = edit.outputText;
}
Edit(int fromOffset, int fromLength, int toOffset, int toLength, String text) {
this.fromOffset = fromOffset;
this.fromLength = fromLength;
this.toOffset = toOffset;
this.toLength = toLength;
this.outputText = text;
}
private final int fromOffset;
private final int fromLength;
private int toOffset;
private final int toLength;
private final String outputText;
@Override
public String toString() {
return "Edit{" +
"from=" + fromOffset + ":" + fromLength +
", to=" + toOffset + ":" + toLength +
((outputText != null) ? (", text='" + outputText + '\'') : "") +
'}';
}
}
protected interface OffsetMapper {
int getInputOffset(int outputOffset);
int getOutputOffset(int inputOffset);
OffsetMapper thenMapping(OffsetMapper mapper);
OffsetMapper EMPTY_MAPPER = CompositeOffsetMapper.of();
}
private static class SimpleOffsetMapper implements OffsetMapper {
private List<Edit> inMap = new ArrayList<>();
private List<Edit> outMap = new ArrayList<>();
private int outputOffsetOfInputStart;
private int inputOffsetOfOutputStart;
private SimpleOffsetMapper(List<Edit> inMap, List<Edit> outMap) {
this.inMap.addAll(inMap);
this.outMap.addAll(outMap);
Edit inStart = null;
for (Edit in : this.inMap) {
inStart = in;
if (in.fromLength > 0) break;
}
outputOffsetOfInputStart = inStart == null ? 0 : inStart.toOffset;
Edit outStart = null;
for (Edit out : this.inMap) {
outStart = out;
if (out.toLength > 0) break;
}
inputOffsetOfOutputStart = outStart == null ? 0 : outStart.fromOffset;
}
@Override
public int getInputOffset(int outputOffset) {
if (outputOffset < outputOffsetOfInputStart) return -1;
Edit searchKey = new Edit(0, 0, outputOffset, Integer.MAX_VALUE, null);
int i = Collections.binarySearch(outMap, searchKey, OUTPUT_OFFSET_COMP);
if (i < 0) {
i = -(i + 1);
i -= 1;
}
i = PApplet.constrain(i, 0, outMap.size()-1);
Edit edit = outMap.get(i);
int diff = outputOffset - edit.toOffset;
return edit.fromOffset + Math.min(diff, Math.max(0, edit.fromLength - 1));
}
@Override
public int getOutputOffset(int inputOffset) {
if (inputOffset < inputOffsetOfOutputStart) return -1;
Edit searchKey = new Edit(inputOffset, Integer.MAX_VALUE, 0, 0, null);
int i = Collections.binarySearch(inMap, searchKey, INPUT_OFFSET_COMP);
if (i < 0) {
i = -(i + 1);
i -= 1;
}
i = PApplet.constrain(i, 0, inMap.size()-1);
Edit edit = inMap.get(i);
int diff = inputOffset - edit.fromOffset;
return edit.toOffset + Math.min(diff, Math.max(0, edit.toLength - 1));
}
@Override
public OffsetMapper thenMapping(OffsetMapper mapper) {
return CompositeOffsetMapper.of(this, mapper);
}
}
private static class CompositeOffsetMapper implements OffsetMapper {
private List<OffsetMapper> mappers = new ArrayList<>();
public static CompositeOffsetMapper of(OffsetMapper... inMappers) {
CompositeOffsetMapper composite = new CompositeOffsetMapper();
// Add mappers one by one, unwrap if Composite
for (OffsetMapper mapper : inMappers) {
if (mapper instanceof CompositeOffsetMapper) {
composite.mappers.addAll(((CompositeOffsetMapper) mapper).mappers);
} else {
composite.mappers.add(mapper);
}
}
return composite;
}
@Override
public int getInputOffset(int outputOffset) {
for (int i = mappers.size() - 1; i >= 0; i--) {
outputOffset = mappers.get(i).getInputOffset(outputOffset);
}
return outputOffset;
}
@Override
public int getOutputOffset(int inputOffset) {
for (OffsetMapper mapper : mappers) {
inputOffset = mapper.getOutputOffset(inputOffset);
}
return inputOffset;
}
@Override
public OffsetMapper thenMapping(OffsetMapper mapper) {
return CompositeOffsetMapper.of(this, mapper);
}
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/processing-app/test/src/test/processing/mode/java/ParserTests.java"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="1"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.preferred_launchers">
<mapEntry key="[run]" value="org.eclipse.jdt.junit.launchconfig"/>
</mapAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
</listAttribute>
<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value=""/>
<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="test.processing.mode.java.ParserTests"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="processing-app"/>
</launchConfiguration>
@@ -0,0 +1,776 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
package processing.mode.java.preproc;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.BitSet;
import java.util.Stack;
import processing.app.Preferences;
import processing.app.SketchException;
import processing.mode.java.preproc.PdeTokenTypes;
import antlr.CommonASTWithHiddenTokens;
import antlr.CommonHiddenStreamToken;
import antlr.collections.AST;
/* Based on original code copyright (c) 2003 Andy Tripp <atripp@comcast.net>.
* shipped under GPL with permission.
*/
/**
* PDEEmitter: A class that can take an ANTLR Java AST and produce
* reasonably formatted Java code from it. To use it, create a
* PDEEmitter object, call setOut() if you want to print to something
* other than System.out, and then call print(), passing the
* AST. Typically, the AST node that you pass would be the root of a
* tree - the ROOT_ID node that represents a Java file.
*
* Modified March 2010 to support Java 5 type arguments and for loops by
* @author Jonathan Feinberg &lt;jdf@pobox.com&gt;
*/
@SuppressWarnings("serial")
public class PdeEmitter implements PdeTokenTypes {
private final PdePreprocessor pdePreprocessor;
private final PrintWriter out;
private final PrintStream debug = System.err;
private final Stack<AST> stack = new Stack<AST>();
private final static int ROOT_ID = 0;
public PdeEmitter(final PdePreprocessor pdePreprocessor, final PrintWriter out) {
this.pdePreprocessor = pdePreprocessor;
this.out = out;
}
/**
* Find a child of the given AST that has the given type
* @returns a child AST of the given type. If it can't find a child of the
* given type, return null.
*/
static private AST getChild(final AST ast, final int childType) {
AST child = ast.getFirstChild();
while (child != null) {
if (child.getType() == childType) {
// debug.println("getChild: found:" + name(ast));
return child;
}
child = child.getNextSibling();
}
return null;
}
/**
* Dump the list of hidden tokens linked to after the AST node passed in.
* Most hidden tokens are dumped from this function.
*/
private void dumpHiddenAfter(final AST ast) {
dumpHiddenTokens(((CommonASTWithHiddenTokens) ast).getHiddenAfter());
}
/**
* Dump the list of hidden tokens linked to before the AST node passed in.
* The only time hidden tokens need to be dumped with this function is when
* dealing parts of the tree where automatic tree construction was
* turned off with the ! operator in the grammar file and the nodes were
* manually constructed in such a way that the usual tokens don't have the
* necessary hiddenAfter links.
*/
private void dumpHiddenBefore(final AST ast) {
antlr.CommonHiddenStreamToken child = null, parent = ((CommonASTWithHiddenTokens) ast)
.getHiddenBefore();
// if there aren't any hidden tokens here, quietly return
//
if (parent == null) {
return;
}
// traverse back to the head of the list of tokens before this node
do {
child = parent;
parent = child.getHiddenBefore();
} while (parent != null);
// dump that list
dumpHiddenTokens(child);
}
/**
* Dump the list of hidden tokens linked to from the token passed in.
*/
private void dumpHiddenTokens(CommonHiddenStreamToken t) {
for (; t != null; t = pdePreprocessor.getHiddenAfter(t)) {
out.print(t.getText());
}
}
/**
* Print the children of the given AST
* @param ast The AST to print
* @returns true iff anything was printed
*/
private boolean printChildren(final AST ast) throws SketchException {
boolean ret = false;
AST child = ast.getFirstChild();
while (child != null) {
ret = true;
print(child);
child = child.getNextSibling();
}
return ret;
}
/**
* Tells whether an AST has any children or not.
* @return true iff the AST has at least one child
*/
static private boolean hasChildren(final AST ast) {
return (ast.getFirstChild() != null);
}
/**
* Gets the best node in the subtree for printing. This really means
* the next node which could potentially have hiddenBefore data. It's
* usually the first printable leaf, but not always.
*
* @param includeThisNode Should this node be included in the search?
* If false, only descendants are searched.
*
* @return the first printable leaf node in an AST
*/
private AST getBestPrintableNode(final AST ast, final boolean includeThisNode) {
AST child;
if (includeThisNode) {
child = ast;
} else {
child = ast.getFirstChild();
}
if (child != null) {
switch (child.getType()) {
// the following node types are printing nodes that print before
// any children, but then also recurse over children. So they
// may have hiddenBefore chains that need to be printed first. Many
// statements and all unary expression types qualify. Return these
// nodes directly
case CLASS_DEF:
case ENUM_DEF:
case LITERAL_if:
case LITERAL_new:
case LITERAL_for:
case LITERAL_while:
case LITERAL_do:
case LITERAL_break:
case LITERAL_continue:
case LITERAL_return:
case LITERAL_switch:
case LITERAL_try:
case LITERAL_throw:
case LITERAL_synchronized:
case LITERAL_assert:
case BNOT:
case LNOT:
case INC:
case DEC:
case UNARY_MINUS:
case UNARY_PLUS:
return child;
// Some non-terminal node types (at the moment, I only know of
// MODIFIERS, but there may be other such types), can be
// leaves in the tree but not have any children. If this is
// such a node, move on to the next sibling.
case MODIFIERS:
if (child.getFirstChild() == null) {
return getBestPrintableNode(child.getNextSibling(), false);
}
// new jikes doesn't like fallthrough, so just duplicated here:
return getBestPrintableNode(child, false);
default:
return getBestPrintableNode(child, false);
}
}
return ast;
}
// Because the meanings of <, >, >>, and >>> are overloaded to support
// type arguments and type parameters, we have to treat them
// as copyable to hidden text (or else the following syntax,
// such as (); and what not gets lost under certain circumstances
//
// Since they are copied to the hidden stream, you don't want
// to print them explicitly; they come out in the dumpHiddenXXX methods.
// -- jdf
private static final BitSet OTHER_COPIED_TOKENS = new BitSet() {
{
set(LT);
set(GT);
set(SR);
set(BSR);
}
};
/**
* Prints a binary operator
*/
private void printBinaryOperator(final AST ast) throws SketchException {
print(ast.getFirstChild());
if (!OTHER_COPIED_TOKENS.get(ast.getType())) {
out.print(ast.getText());
dumpHiddenAfter(ast);
}
print(ast.getFirstChild().getNextSibling());
}
private void printMethodDef(final AST ast) throws SketchException {
final AST modifiers = ast.getFirstChild();
final AST typeParameters, type;
if (modifiers.getNextSibling().getType() == TYPE_PARAMETERS) {
typeParameters = modifiers.getNextSibling();
type = typeParameters.getNextSibling();
} else {
typeParameters = null;
type = modifiers.getNextSibling();
}
final AST methodName = type.getNextSibling();
// if (methodName.getText().equals("main")) {
// pdePreprocessor.setFoundMain(true);
// }
pdePreprocessor.addMethod(methodName.getText());
printChildren(ast);
}
private void printIfThenElse(final AST literalIf) throws SketchException {
out.print(literalIf.getText());
dumpHiddenAfter(literalIf);
final AST condition = literalIf.getFirstChild();
print(condition); // the "if" condition: an EXPR
// the "then" clause is either an SLIST or an EXPR
final AST thenPath = condition.getNextSibling();
print(thenPath);
// optional "else" clause: an SLIST or an EXPR
// what could be simpler?
final AST elsePath = thenPath.getNextSibling();
if (elsePath != null) {
out.print("else");
final AST bestPrintableNode = getBestPrintableNode(elsePath, true);
dumpHiddenBefore(bestPrintableNode);
final CommonHiddenStreamToken hiddenBefore =
((CommonASTWithHiddenTokens) elsePath).getHiddenBefore();
if (elsePath.getType() == PdeTokenTypes.SLIST && elsePath.getNumberOfChildren() == 0 &&
hiddenBefore == null) {
out.print("{");
final CommonHiddenStreamToken hiddenAfter =
((CommonASTWithHiddenTokens) elsePath).getHiddenAfter();
if (hiddenAfter == null) {
out.print("}");
} else {
dumpHiddenTokens(hiddenAfter);
}
} else {
print(elsePath);
}
}
}
/**
* Print the given AST. Call this function to print your PDE code.
*
* It works by making recursive calls to print children.
* So the code below is one big "switch" statement on the passed AST type.
*/
public void print(final AST ast) throws SketchException {
if (ast == null) {
return;
}
stack.push(ast);
final AST child1 = ast.getFirstChild();
AST child2 = null;
AST child3 = null;
if (child1 != null) {
child2 = child1.getNextSibling();
if (child2 != null) {
child3 = child2.getNextSibling();
}
}
switch (ast.getType()) {
// The top of the tree looks like this:
// ROOT_ID "Whatever.java"
// package
// imports
// class definition
case ROOT_ID:
dumpHiddenTokens(pdePreprocessor.getInitialHiddenToken());
printChildren(ast);
break;
// supporting a "package" statement in a PDE program has
// a bunch of issues with it that need to dealt in the compilation
// code too, so this isn't actually tested.
case PACKAGE_DEF:
out.print("package");
dumpHiddenAfter(ast);
print(ast.getFirstChild());
break;
// IMPORT has exactly one child
case IMPORT:
out.print("import");
dumpHiddenAfter(ast);
print(ast.getFirstChild());
break;
case STATIC_IMPORT:
out.print("import static");
dumpHiddenAfter(ast);
print(ast.getFirstChild());
break;
case CLASS_DEF:
case ENUM_DEF:
case INTERFACE_DEF:
print(getChild(ast, MODIFIERS));
if (ast.getType() == CLASS_DEF) {
out.print("class");
} else if (ast.getType() == ENUM_DEF) {
out.print("enum");
} else {
out.print("interface");
}
dumpHiddenBefore(getChild(ast, IDENT));
print(getChild(ast, IDENT));
print(getChild(ast, TYPE_PARAMETERS));
print(getChild(ast, EXTENDS_CLAUSE));
print(getChild(ast, IMPLEMENTS_CLAUSE));
print(getChild(ast, OBJBLOCK));
break;
case EXTENDS_CLAUSE:
if (hasChildren(ast)) {
out.print("extends");
dumpHiddenBefore(getBestPrintableNode(ast, false));
printChildren(ast);
}
break;
case IMPLEMENTS_CLAUSE:
if (hasChildren(ast)) {
out.print("implements");
dumpHiddenBefore(getBestPrintableNode(ast, false));
printChildren(ast);
}
break;
// DOT
case DOT:
print(child1);
out.print(".");
dumpHiddenAfter(ast);
print(child2);
if (child3 != null) {
print(child3);
}
break;
case MODIFIERS:
case OBJBLOCK:
case CTOR_DEF:
//case METHOD_DEF:
case PARAMETERS:
case PARAMETER_DEF:
case VARIABLE_PARAMETER_DEF:
case VARIABLE_DEF:
case ENUM_CONSTANT_DEF:
case TYPE:
case SLIST:
case ELIST:
case ARRAY_DECLARATOR:
case TYPECAST:
case EXPR:
case ARRAY_INIT:
case FOR_INIT:
case FOR_CONDITION:
case FOR_ITERATOR:
case METHOD_CALL:
case INSTANCE_INIT:
case INDEX_OP:
case SUPER_CTOR_CALL:
case CTOR_CALL:
printChildren(ast);
break;
case METHOD_DEF:
printMethodDef(ast);
break;
// if we have two children, it's of the form "a=0"
// if just one child, it's of the form "=0" (where the
// lhs is above this AST).
case ASSIGN:
if (child2 != null) {
print(child1);
out.print("=");
dumpHiddenAfter(ast);
print(child2);
} else {
out.print("=");
dumpHiddenAfter(ast);
print(child1);
}
break;
// binary operators:
case PLUS:
case MINUS:
case DIV:
case MOD:
case NOT_EQUAL:
case EQUAL:
case LE:
case GE:
case LOR:
case LAND:
case BOR:
case BXOR:
case BAND:
case SL:
case SR:
case BSR:
case LITERAL_instanceof:
case PLUS_ASSIGN:
case MINUS_ASSIGN:
case STAR_ASSIGN:
case DIV_ASSIGN:
case MOD_ASSIGN:
case SR_ASSIGN:
case BSR_ASSIGN:
case SL_ASSIGN:
case BAND_ASSIGN:
case BXOR_ASSIGN:
case BOR_ASSIGN:
case LT:
case GT:
printBinaryOperator(ast);
break;
case LITERAL_for:
out.print(ast.getText());
dumpHiddenAfter(ast);
if (child1.getType() == FOR_EACH_CLAUSE) {
printChildren(child1);
print(child2);
} else {
printChildren(ast);
}
break;
case POST_INC:
case POST_DEC:
print(child1);
out.print(ast.getText());
dumpHiddenAfter(ast);
break;
// unary operators:
case BNOT:
case LNOT:
case INC:
case DEC:
case UNARY_MINUS:
case UNARY_PLUS:
out.print(ast.getText());
dumpHiddenAfter(ast);
print(child1);
break;
case LITERAL_new:
out.print("new");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LITERAL_return:
out.print("return");
dumpHiddenAfter(ast);
print(child1);
break;
case STATIC_INIT:
out.print("static");
dumpHiddenBefore(getBestPrintableNode(ast, false));
print(child1);
break;
case LITERAL_switch:
out.print("switch");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LABELED_STAT:
case CASE_GROUP:
printChildren(ast);
break;
case LITERAL_case:
out.print("case");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LITERAL_default:
out.print("default");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case NUM_INT:
case CHAR_LITERAL:
case STRING_LITERAL:
case NUM_FLOAT:
case NUM_LONG:
out.print(ast.getText());
dumpHiddenAfter(ast);
break;
case LITERAL_synchronized: // 0137 to fix bug #136
case LITERAL_assert:
out.print(ast.getText());
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LITERAL_private:
case LITERAL_public:
case LITERAL_protected:
case LITERAL_static:
case LITERAL_transient:
case LITERAL_native:
case LITERAL_threadsafe:
//case LITERAL_synchronized: // 0137 to fix bug #136
case LITERAL_volatile:
case LITERAL_class: // 0176 to fix bug #1466
case FINAL:
case ABSTRACT:
case LITERAL_package:
case LITERAL_void:
case LITERAL_boolean:
case LITERAL_byte:
case LITERAL_char:
case LITERAL_short:
case LITERAL_int:
case LITERAL_float:
case LITERAL_long:
case LITERAL_double:
case LITERAL_true:
case LITERAL_false:
case LITERAL_null:
case SEMI:
case LITERAL_this:
case LITERAL_super:
out.print(ast.getText());
dumpHiddenAfter(ast);
break;
case EMPTY_STAT:
case EMPTY_FIELD:
break;
case LITERAL_continue:
case LITERAL_break:
out.print(ast.getText());
dumpHiddenAfter(ast);
if (child1 != null) {// maybe label
print(child1);
}
break;
// yuck: Distinguish between "import x.y.*" and "x = 1 * 3"
case STAR:
if (hasChildren(ast)) { // the binary mult. operator
printBinaryOperator(ast);
} else { // the special "*" in import:
out.print("*");
dumpHiddenAfter(ast);
}
break;
case LITERAL_throws:
out.print("throws");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LITERAL_if:
printIfThenElse(ast);
break;
case LITERAL_while:
out.print("while");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LITERAL_do:
out.print("do");
dumpHiddenAfter(ast);
print(child1); // an SLIST
out.print("while");
dumpHiddenBefore(getBestPrintableNode(child2, false));
print(child2); // an EXPR
break;
case LITERAL_try:
out.print("try");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LITERAL_catch:
out.print("catch");
dumpHiddenAfter(ast);
printChildren(ast);
break;
// the first child is the "try" and the second is the SLIST
case LITERAL_finally:
out.print("finally");
dumpHiddenAfter(ast);
printChildren(ast);
break;
case LITERAL_throw:
out.print("throw");
dumpHiddenAfter(ast);
print(child1);
break;
// the dreaded trinary operator
case QUESTION:
print(child1);
out.print("?");
dumpHiddenAfter(ast);
print(child2);
print(child3);
break;
// pde specific or modified tokens start here
// Image -> BImage, Font -> BFont as appropriate
case IDENT:
/*
if (ast.getText().equals("Image") &&
Preferences.getBoolean("preproc.substitute_image")) { //, true)) {
out.print("BImage");
} else if (ast.getText().equals("Font") &&
Preferences.getBoolean("preproc.substitute_font")) { //, true)) {
out.print("BFont");
} else {
*/
out.print(ast.getText());
//}
dumpHiddenAfter(ast);
break;
// the color datatype is just an alias for int
case LITERAL_color:
out.print("int");
dumpHiddenAfter(ast);
break;
case WEBCOLOR_LITERAL:
if (ast.getText().length() != 6) {
System.err.println("Internal error: incorrect length of webcolor "
+ "literal should have been detected sooner.");
break;
}
out.print("0xff" + ast.getText());
dumpHiddenAfter(ast);
break;
// allow for stuff like int(43.2).
case CONSTRUCTOR_CAST:
final AST terminalTypeNode = child1.getFirstChild();
final AST exprToCast = child2;
final String pooType = terminalTypeNode.getText();
out.print("PApplet.parse" + Character.toUpperCase(pooType.charAt(0))
+ pooType.substring(1));
dumpHiddenAfter(terminalTypeNode); // the left paren
print(exprToCast);
break;
// making floating point literals default to floats, not doubles
case NUM_DOUBLE:
final String literalDouble = ast.getText().toLowerCase();
out.print(literalDouble);
if (Preferences.getBoolean("preproc.substitute_floats")
&& literalDouble.indexOf('d') == -1) { // permit literal doubles
out.print("f");
}
dumpHiddenAfter(ast);
break;
case TYPE_ARGUMENTS:
case TYPE_PARAMETERS:
printChildren(ast);
break;
case TYPE_ARGUMENT:
case TYPE_PARAMETER:
printChildren(ast);
break;
case WILDCARD_TYPE:
out.print(ast.getText());
dumpHiddenAfter(ast);
print(ast.getFirstChild());
break;
case TYPE_LOWER_BOUNDS:
case TYPE_UPPER_BOUNDS:
out.print(ast.getType() == TYPE_LOWER_BOUNDS ? "super" : "extends");
dumpHiddenBefore(getBestPrintableNode(ast, false));
printChildren(ast);
break;
case ANNOTATION:
out.print("@");
printChildren(ast);
break;
case ANNOTATIONS:
case ANNOTATION_ARRAY_INIT:
printChildren(ast);
break;
case ANNOTATION_MEMBER_VALUE_PAIR:
print(ast.getFirstChild());
out.print("=");
dumpHiddenBefore(getBestPrintableNode(ast.getFirstChild().getNextSibling(), false));
print(ast.getFirstChild().getNextSibling());
break;
default:
debug.println("Unrecognized type:" + ast.getType() + " ("
+ TokenUtil.nameOf(ast) + ")");
break;
}
stack.pop();
}
}
@@ -0,0 +1,32 @@
package processing.mode.java.preproc;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import processing.app.SketchException;
/**
*
* @author Jonathan Feinberg &lt;jdf@pobox.com&gt;
*
*/
public class PreprocessorResult {
public final int headerOffset;
public final String className;
public final List<String> extraImports;
public final PdePreprocessor.Mode programType;
public PreprocessorResult(PdePreprocessor.Mode programType,
int headerOffset, String className,
final List<String> extraImports) throws SketchException {
if (className == null) {
throw new SketchException("Could not find main class");
}
this.headerOffset = headerOffset;
this.className = className;
this.extraImports = Collections.unmodifiableList(new ArrayList<String>(extraImports));
this.programType = programType;
}
}
@@ -0,0 +1,152 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
SizeInfo - parsed elements of a size() or fullScreen() call
Part of the Processing project - http://processing.org
Copyright (c) 2015 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 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.java.preproc;
import processing.app.Messages;
import processing.core.PApplet;
import processing.data.StringList;
public class SurfaceInfo {
StringList statements = new StringList();
String width;
String height;
String renderer;
String path;
String display;
/** null for nothing in setup(), 0 for noSmooth(), N for smooth(N) */
//Integer quality;
// String smooth;
boolean hasOldSyntax() {
if (width.equals("screenWidth") ||
width.equals("screenHeight") ||
height.equals("screenHeight") ||
height.equals("screenWidth")) {
final String message =
"The screenWidth and screenHeight variables are named\n" +
"displayWidth and displayHeight in Processing 3.\n" +
"Or you can use the fullScreen() method instead of size().";
Messages.showWarning("Time for a quick update", message, null);
return true;
}
if (width.equals("screen.width") ||
width.equals("screen.height") ||
height.equals("screen.height") ||
height.equals("screen.width")) {
final String message =
"The screen.width and screen.height variables are named\n" +
"displayWidth and displayHeight in Processing 3.\n" +
"Or you can use the fullScreen() method instead of size().";
Messages.showWarning("Time for a quick update", message, null);
return true;
}
return false;
}
boolean hasBadSize() {
if (!width.equals("displayWidth") &&
!width.equals("displayHeight") &&
PApplet.parseInt(width, -1) == -1) {
return true;
}
if (!height.equals("displayWidth") &&
!height.equals("displayHeight") &&
PApplet.parseInt(height, -1) == -1) {
return true;
}
return false;
}
void checkEmpty() {
if (renderer != null) {
if (renderer.length() == 0) { // if empty, set null
renderer = null;
}
}
if (path != null) {
if (path.length() == 0) {
path = null;
}
}
if (display != null) {
if (display.length() == 0) {
display = null;
}
}
}
// public String getStatements() {
// return statements.join(" ");
// }
public StringList getStatements() {
return statements;
}
/**
* Add an item that will be moved from size() into the settings() method.
* This needs to be the exact version of the statement so that it can be
* matched against and removed from the size() method in the code.
*/
public void addStatement(String stmt) {
statements.append(stmt);
}
public void addStatements(StringList list) {
statements.append(list);
}
/** @return true if there's code to be inserted for a settings() method. */
public boolean hasSettings() {
return statements.size() != 0;
}
/** @return the contents of the settings() method to be inserted */
public String getSettings() {
return statements.join(" ");
}
// Added for Android Mode to check whether OpenGL is in use
// https://github.com/processing/processing/issues/4441
/**
* Return the renderer specified (null if none specified).
* @since 3.2.2
*/
public String getRenderer() {
return renderer;
}
}
@@ -0,0 +1,30 @@
package processing.mode.java.preproc;
import java.lang.reflect.Field;
import antlr.collections.AST;
import processing.mode.java.preproc.PdeTokenTypes;
/**
*
* @author Jonathan Feinberg &lt;jdf@pobox.com&gt;
*
*/
public class TokenUtil {
private static final String[] tokenNames= new String[200];
static {
for (int i = 0; i < tokenNames.length; i++) {
tokenNames[i] = "ERROR:" + i;
}
for (final Field f : PdeTokenTypes.class.getDeclaredFields()) {
try {
tokenNames[f.getInt(null)] = f.getName();
} catch (Exception unexpected) {
throw new RuntimeException(unexpected);
}
}
}
public static String nameOf(final AST node) {
return tokenNames[node.getType()];
}
}
@@ -0,0 +1,394 @@
/* -*- mode: antlr; c-basic-offset: 4; indent-tabs-mode: nil -*- */
header {
package processing.mode.java.preproc;
}
class PdeRecognizer extends JavaRecognizer;
options {
importVocab = Java;
exportVocab = PdePartial;
//codeGenMakeSwitchThreshold=10; // this is set high for debugging
//codeGenBitsetTestThreshold=10; // this is set high for debugging
// developers may to want to set this to true for better
// debugging messages, however, doing so disables highlighting errors
// in the editor.
defaultErrorHandler = false; //true;
}
tokens {
CONSTRUCTOR_CAST; EMPTY_FIELD;
}
{
// this clause copied from java15.g! ANTLR does not copy this
// section from the super grammar.
/**
* Counts the number of LT seen in the typeArguments production.
* It is used in semantic predicates to ensure we have seen
* enough closing '>' characters; which actually may have been
* either GT, SR or BSR tokens.
*/
private int ltCounter = 0;
private PdePreprocessor pp;
public PdeRecognizer(final PdePreprocessor pp, final TokenStream ts) {
this(ts);
this.pp = pp;
}
private void mixed() throws RecognitionException, TokenStreamException {
throw new RecognitionException("It looks like you're mixing \"active\" and \"static\" modes.",
getFilename(), LT(1).getLine(), LT(1).getColumn());
}
}
pdeProgram
:
// Some programs can be equally well interpreted as STATIC or ACTIVE;
// this forces the parser to prefer the STATIC interpretation.
(staticProgram) => staticProgram
{ pp.setMode(PdePreprocessor.Mode.STATIC); }
| (activeProgram) => activeProgram
{ pp.setMode(PdePreprocessor.Mode.ACTIVE); }
| staticProgram
{ pp.setMode(PdePreprocessor.Mode.STATIC); }
;
// advanced mode is really just a normal java file
javaProgram
: compilationUnit
;
activeProgram
: (
(IDENT LPAREN) => IDENT LPAREN { mixed(); }
| possiblyEmptyField
)+ EOF!
;
staticProgram
: (
statement
)* EOF!
;
// copy of the java.g rule with WEBCOLOR_LITERAL added
constant
: NUM_INT
| CHAR_LITERAL
| STRING_LITERAL
| NUM_FLOAT
| NUM_LONG
| NUM_DOUBLE
| webcolor_literal
;
// fix bug http://dev.processing.org/bugs/show_bug.cgi?id=1519
// by altering a syntactic predicate whose sole purpose is to
// emit a useless error with no line numbers.
// These are from Java15.g, with a few lines edited to make nice errors.
// Type arguments to a class or interface type
typeArguments
{int currentLtLevel = 0;}
:
{currentLtLevel = ltCounter;}
LT! {ltCounter++;}
typeArgument
(options{greedy=true;}: // match as many as possible
{if (! (inputState.guessing !=0 || ltCounter == currentLtLevel + 1)) {
throw new RecognitionException("Maybe too many > characters?",
getFilename(), LT(1).getLine(), LT(1).getColumn());
}}
COMMA! typeArgument
)*
( // turn warning off since Antlr generates the right code,
// plus we have our semantic predicate below
options{generateAmbigWarnings=false;}:
typeArgumentsOrParametersEnd
)?
// make sure we have gobbled up enough '>' characters
// if we are at the "top level" of nested typeArgument productions
{if (! ((currentLtLevel != 0) || ltCounter == currentLtLevel)) {
throw new RecognitionException("Maybe too many > characters?",
getFilename(), LT(1).getLine(), LT(1).getColumn());
}}
{#typeArguments = #(#[TYPE_ARGUMENTS, "TYPE_ARGUMENTS"], #typeArguments);}
;
typeParameters
{int currentLtLevel = 0;}
:
{currentLtLevel = ltCounter;}
LT! {ltCounter++;}
typeParameter (COMMA! typeParameter)*
(typeArgumentsOrParametersEnd)?
// make sure we have gobbled up enough '>' characters
// if we are at the "top level" of nested typeArgument productions
{if (! ((currentLtLevel != 0) || ltCounter == currentLtLevel)) {
throw new RecognitionException("Maybe too many > characters?",
getFilename(), LT(1).getLine(), LT(1).getColumn());
}}
{#typeParameters = #(#[TYPE_PARAMETERS, "TYPE_PARAMETERS"], #typeParameters);}
;
// this gobbles up *some* amount of '>' characters, and counts how many
// it gobbled.
protected typeArgumentsOrParametersEnd
: GT! {ltCounter-=1;}
| SR! {ltCounter-=2;}
| BSR! {ltCounter-=3;}
;
// of the form #cc008f in PDE
webcolor_literal
: w:WEBCOLOR_LITERAL
{ if (! (processing.app.Preferences.getBoolean("preproc.web_colors")
&&
w.getText().length() == 6)) {
throw new RecognitionException("Web colors must be exactly 6 hex digits. This looks like " + w.getText().length() + ".",
getFilename(), LT(1).getLine(), LT(1).getColumn());
}} // must be exactly 6 hex digits
;
// copy of the java.g builtInType rule
builtInConsCastType
: "void"
| "boolean"
| "byte"
| "char"
| "short"
| "int"
| "float"
| "long"
| "double"
;
// our types include the java types and "color". this is separated into two
// rules so that constructor casts can just use the original typelist, since
// we don't want to support the color type as a constructor cast.
//
builtInType
: builtInConsCastType
| "color" // aliased to an int in PDE
{ processing.app.Preferences.getBoolean("preproc.color_datatype") }?
;
// constructor style casts.
constructorCast!
: t:consCastTypeSpec[true]
LPAREN!
e:expression
RPAREN!
// if this is a string literal, make sure the type we're trying to cast
// to is one of the supported ones
//
{ (#e == null) ||
( (#e.getType() != STRING_LITERAL) ||
( #t.getType() == LITERAL_boolean ||
#t.getType() == LITERAL_double ||
#t.getType() == LITERAL_float ||
#t.getType() == LITERAL_int ||
#t.getType() == LITERAL_long ||
#t.getType() == LITERAL_short )) }?
// create the node
//
{#constructorCast = #(#[CONSTRUCTOR_CAST,"CONSTRUCTOR_CAST"], t, e);}
;
// A list of types that be used as the destination type in a constructor-style
// cast. Ideally, this would include all class types, not just "String".
// Unfortunately, it's not possible to tell whether Foo(5) is supposed to be
// a method call or a constructor cast without have a table of all valid
// types or methods, which requires semantic analysis (eg processing of import
// statements). So we accept the set of built-in types plus "String".
//
consCastTypeSpec[boolean addImagNode]
// : stringTypeSpec[addImagNode]
// | builtInConsCastTypeSpec[addImagNode]
: builtInConsCastTypeSpec[addImagNode]
// trying to remove String() cast [fry]
;
//stringTypeSpec[boolean addImagNode]
// : id:IDENT { #id.getText().equals("String") }?
// {
// if ( addImagNode ) {
// #stringTypeSpec = #(#[TYPE,"TYPE"],
// #stringTypeSpec);
// }
// }
// ;
builtInConsCastTypeSpec[boolean addImagNode]
: builtInConsCastType
{
if ( addImagNode ) {
#builtInConsCastTypeSpec = #(#[TYPE,"TYPE"],
#builtInConsCastTypeSpec);
}
}
;
// Since "color" tokens are lexed as LITERAL_color now, we need to have a rule
// that can generate a method call from an expression that starts with this
// token
//
colorMethodCall
: c:"color" {#c.setType(IDENT);} // this would default to LITERAL_color
lp:LPAREN^ {#lp.setType(METHOD_CALL);}
argList
RPAREN!
;
// copy of the java.g rule with added constructorCast and colorMethodCall
// alternatives
primaryExpression
: (consCastTypeSpec[false] LPAREN) => constructorCast
{ processing.app.Preferences.getBoolean("preproc.enhanced_casting") }?
| identPrimary ( options {greedy=true;} : DOT^ "class" )?
| constant
| "true"
| "false"
| "null"
| newExpression
| "this"
| "super"
| LPAREN! assignmentExpression RPAREN!
| colorMethodCall
// look for int.class and int[].class
| builtInType
( lbt:LBRACK^ {#lbt.setType(ARRAY_DECLARATOR);} RBRACK! )*
DOT^ "class"
;
// the below variable rule hacks are needed so that it's possible for the
// emitter to correctly output variable declarations of the form "float a, b"
// from the AST. This means that our AST has a somewhat different form in
// these rules than the java one does, and this new form may have its own
// semantic issues. But it seems to fix the comma declaration issues.
//
variableDefinitions![AST mods, AST t]
: vd:variableDeclarator[getASTFactory().dupTree(mods),
getASTFactory().dupTree(t)]
{#variableDefinitions = #(#[VARIABLE_DEF,"VARIABLE_DEF"], mods,
t, vd);}
;
variableDeclarator[AST mods, AST t]
: ( id:IDENT (lb:LBRACK^ {#lb.setType(ARRAY_DECLARATOR);} RBRACK!)*
v:varInitializer (COMMA!)? )+
;
// java.g builds syntax trees with an inconsistent structure. override one of
// the rules there to fix this.
//
explicitConstructorInvocation!
: (typeArguments)?
t:"this" LPAREN a1:argList RPAREN SEMI
{#explicitConstructorInvocation = #(#[CTOR_CALL, "CTOR_CALL"],
#t, #a1);}
| s:"super" LPAREN a2:argList RPAREN SEMI
{#explicitConstructorInvocation = #(#[SUPER_CTOR_CALL,
"SUPER_CTOR_CALL"],
#s, #a2);}
;
// quick-n-dirty hack to the get the advanced class name. we should
// really be getting it from the AST and not forking this rule from
// the java.g copy at all. Since this is a recursive descent parser, we get
// the last class name in the file so that we don't end up with the classname
// of an inner class. If there is more than one "outer" class in a file,
// this heuristic will fail.
//
classDefinition![AST modifiers]
: "class" i:IDENT
// it _might_ have type paramaters
(tp:typeParameters)?
// it _might_ have a superclass...
sc:superClassClause
// it might implement some interfaces...
ic:implementsClause
// now parse the body of the class
cb:classBlock
{#classDefinition = #(#[CLASS_DEF,"CLASS_DEF"],
modifiers,i,tp,sc,ic,cb);
pp.setAdvClassName(i.getText());}
;
possiblyEmptyField
: classField
| s:SEMI {#s.setType(EMPTY_FIELD);}
;
class PdeLexer extends JavaLexer;
options {
importVocab=PdePartial;
exportVocab=Pde;
}
// We need to preserve whitespace and commentary instead of ignoring
// like the supergrammar does. Otherwise Jikes won't be able to give
// us error messages that point to the equivalent PDE code.
// WS, SL_COMMENT, ML_COMMENT are copies of the original productions,
// but with the SKIP assigment removed.
WS : ( ' '
| '\t'
| '\f'
// handle newlines
| ( options {generateAmbigWarnings=false;}
: "\r\n" // Evil DOS
| '\r' // Macintosh
| '\n' // Unix (the right way)
)
{ newline(); }
)+
;
// Single-line comments
SL_COMMENT
: "//"
(~('\n'|'\r'))* ('\n'|'\r'('\n')?)
{newline();}
;
// multiple-line comments
ML_COMMENT
: "/*"
( /* '\r' '\n' can be matched in one alternative or by matching
'\r' in one iteration and '\n' in another. I am trying to
handle any flavor of newline that comes in, but the language
that allows both "\r\n" and "\r" and "\n" to all be valid
newline is ambiguous. Consequently, the resulting grammar
must be ambiguous. I'm shutting this warning off.
*/
options {
generateAmbigWarnings=false;
}
:
{ LA(2)!='/' }? '*'
| '\r' '\n' {newline();}
| '\r' {newline();}
| '\n' {newline();}
| ~('*'|'\n'|'\r')
)*
"*/"
;
WEBCOLOR_LITERAL
: '#'! (HEX_DIGIT)+
;
@@ -0,0 +1,42 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2004-06 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.mode.java.runner;
/**
* Interface for dealing with parser/compiler output.
* <P>
* Different instances of MessageStream need to do different things with
* messages. In particular, a stream instance used for parsing output from
* the compiler compiler has to interpret its messages differently than one
* parsing output from the runtime.
* <P>
* Classes which consume messages and do something with them
* should implement this interface.
*/
public interface MessageConsumer {
public void message(String s);
}
@@ -0,0 +1,92 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2004-06 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.mode.java.runner;
import java.io.*;
/**
* Slurps up messages from compiler.
*/
public class MessageSiphon implements Runnable {
BufferedReader streamReader;
Thread thread;
MessageConsumer consumer;
public MessageSiphon(InputStream stream, MessageConsumer consumer) {
this.streamReader = new BufferedReader(new InputStreamReader(stream));
this.consumer = consumer;
thread = new Thread(this);
// don't set priority too low, otherwise exceptions won't
// bubble up in time (i.e. compile errors have a weird delay)
//thread.setPriority(Thread.MIN_PRIORITY);
thread.setPriority(Thread.MAX_PRIORITY-1);
//thread.start();
}
public void run() {
try {
// process data until we hit EOF; this will happily block
// (effectively sleeping the thread) until new data comes in.
// when the program is finally done, null will come through.
//
String currentLine;
while ((currentLine = streamReader.readLine()) != null) {
// \n is added again because readLine() strips it out
//EditorConsole.systemOut.println("messaging in");
consumer.message(currentLine + "\n");
//EditorConsole.systemOut.println("messaging out");
}
//EditorConsole.systemOut.println("messaging thread done");
thread = null;
} catch (NullPointerException npe) {
// Fairly common exception during shutdown
thread = null;
} catch (Exception e) {
// On Linux and sometimes on Mac OS X, a "bad file descriptor"
// message comes up when closing an applet that's run externally.
// That message just gets supressed here..
String mess = e.getMessage();
if ((mess != null) &&
(mess.indexOf("Bad file descriptor") != -1)) {
//if (e.getMessage().indexOf("Bad file descriptor") == -1) {
//System.err.println("MessageSiphon err " + e);
//e.printStackTrace();
} else {
e.printStackTrace();
}
thread = null;
}
}
public Thread getThread() {
return thread;
}
}
@@ -0,0 +1,62 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2004-08 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.mode.java.runner;
import java.io.*;
/**
* OutputStream to handle stdout/stderr messages.
* <P>
* This is used by Editor, System.err is set to
* new PrintStream(new MessageStream()).
* It's also used by Compiler.
*/
class MessageStream extends OutputStream {
MessageConsumer messageConsumer;
public MessageStream(MessageConsumer messageConsumer) {
this.messageConsumer = messageConsumer;
}
public void close() { }
public void flush() { }
public void write(byte b[]) {
// this never seems to get called
System.out.println("leech1: " + new String(b));
}
public void write(byte b[], int offset, int length) {
//System.out.println("leech2: " + new String(b));
this.messageConsumer.message(new String(b, offset, length));
}
public void write(int b) {
// this never seems to get called
System.out.println("leech3: '" + ((char)b) + "'");
}
}
@@ -0,0 +1,953 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2004-13 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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.java.runner;
import processing.app.*;
import processing.app.exec.StreamRedirectThread;
import processing.app.ui.Toolkit;
import processing.core.*;
import processing.data.StringList;
import processing.mode.java.JavaBuild;
import processing.mode.java.JavaEditor;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.io.*;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.connect.Connector.Argument;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;
/**
* Runs a compiled sketch. As of release 0136, all sketches are run externally
* to the environment so that a debugging interface can be used. This opens up
* future options for a decent debugger, but in the meantime fixes several
* problems with output and error streams, messages getting lost on Mac OS X,
* the run/stop buttons not working, libraries not shutting down, exceptions
* not coming through, exceptions being printed twice, having to force quit
* if you make a bad while() loop, and so on.
*/
public class Runner implements MessageConsumer {
// private boolean presenting;
// Object that listens for error messages or exceptions.
protected RunnerListener listener;
// Running remote VM
protected volatile VirtualMachine vm;
protected boolean vmReturnedError;
// Thread transferring remote error stream to our error stream
protected Thread errThread = null;
// Thread transferring remote output stream to our output stream
protected Thread outThread = null;
protected SketchException exception;
protected JavaEditor editor;
protected JavaBuild build;
protected Process process;
protected PrintStream sketchErr;
protected PrintStream sketchOut;
protected volatile boolean cancelled;
protected final Object cancelLock = new Object[0];
public Runner(JavaBuild build, RunnerListener listener) throws SketchException {
this.listener = listener;
// this.sketch = sketch;
this.build = build;
checkLocalHost();
if (listener instanceof JavaEditor) {
this.editor = (JavaEditor) listener;
sketchErr = editor.getConsole().getErr();
sketchOut = editor.getConsole().getOut();
} else {
sketchErr = System.err;
sketchOut = System.out;
}
// Make sure all the imported libraries will actually run with this setup.
int bits = Platform.getNativeBits();
String variant = Platform.getVariant();
for (Library library : build.getImportedLibraries()) {
if (!library.supportsArch(PApplet.platform, variant)) {
sketchErr.println(library.getName() + " does not run on this architecture: " + variant);
int opposite = (bits == 32) ? 64 : 32;
if (Platform.isMacOS()) {
//if (library.supportsArch(PConstants.MACOSX, opposite)) { // should always be true
throw new SketchException("To use " + library.getName() + ", " +
"switch to " + opposite + "-bit mode in Preferences.");
//}
} else {
throw new SketchException(library.getName() + " is only compatible " +
"with the " + opposite + "-bit download of Processing.");
//throw new SketchException(library.getName() + " does not run in " + bits + "-bit mode.");
// "To use this library, switch to 32-bit mode in Preferences." (OS X)
// "To use this library, you must use the 32-bit version of Processing."
}
}
}
}
/**
* Has the user screwed up their hosts file?
* https://github.com/processing/processing/issues/4738
*/
static private void checkLocalHost() throws SketchException {
try {
InetAddress address = InetAddress.getByName("localhost");
if (!address.getHostAddress().equals("127.0.0.1")) {
System.err.println("Your computer is not properly mapping 'localhost' to '127.0.0.1',");
System.err.println("which prevents sketches from working properly because 'localhost'");
System.err.println("is needed to connect the PDE to your sketch while it's running.");
System.err.println("If you don't recall making this change, or know how to fix it:");
System.err.println("https://www.google.com/search?q=add+localhost+to+hosts+file+" + Platform.getName());
throw new SketchException("Cannot run due to changes in your 'hosts' file. " +
"See the console for details.", false);
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
public VirtualMachine launch(String[] args) {
if (launchVirtualMachine(false, args)) {
generateTrace();
}
return vm;
}
public VirtualMachine present(String[] args) {
if (launchVirtualMachine(true, args)) {
generateTrace();
}
return vm;
}
/**
* Whether the last invocation of launchJava() was successful or not
*/
public boolean vmReturnedError() {
return vmReturnedError;
}
/**
* Simple non-blocking launch of the virtual machine. VM starts suspended.
* @return debuggee VM or null on failure
*/
public VirtualMachine debug(String[] args) {
if (launchVirtualMachine(false, args)) { // will return null on failure
redirectStreams(vm);
}
return vm;
}
/**
* Redirect a VMs output and error streams to System.out and System.err
*/
protected void redirectStreams(VirtualMachine vm) {
MessageSiphon ms = new MessageSiphon(process.getErrorStream(), this);
errThread = ms.getThread();
outThread = new StreamRedirectThread("VM output reader", process.getInputStream(), System.out);
errThread.start();
outThread.start();
}
/**
* Additional access to the virtual machine. TODO: may not be needed
* @return debugge VM or null if not running
*/
public VirtualMachine vm() {
return vm;
}
public boolean launchVirtualMachine(boolean present, String[] args) {
StringList vmParams = getMachineParams();
StringList sketchParams = getSketchParams(present, args);
// PApplet.printArray(sketchParams);
int port = 8000 + (int) (Math.random() * 1000);
String portStr = String.valueOf(port);
// Added 'quiet=y' for 3.0.2 to prevent command line parsing problems
// https://github.com/processing/processing/issues/4098
String jdwpArg = "-agentlib:jdwp=transport=dt_socket,address=" + portStr + ",server=y,suspend=y,quiet=y";
// Everyone works the same under Java 7 (also on OS X)
StringList commandArgs = new StringList();
commandArgs.append(Platform.getJavaPath());
commandArgs.append(jdwpArg);
commandArgs.append(vmParams);
commandArgs.append(sketchParams);
// Opportunistically quit if the launch was cancelled,
// the next chance to cancel will be after connecting to the VM
if (cancelled) {
return false;
}
launchJava(commandArgs.array());
AttachingConnector connector = (AttachingConnector)
findConnector("com.sun.jdi.SocketAttach");
//PApplet.println(connector); // gets the defaults
Map<String, Argument> arguments = connector.defaultArguments();
// Connector.Argument addressArg =
// (Connector.Argument)arguments.get("address");
// addressArg.setValue(addr);
Connector.Argument portArg = arguments.get("port");
portArg.setValue(portStr);
// Connector.Argument timeoutArg =
// (Connector.Argument)arguments.get("timeout");
// timeoutArg.setValue("10000");
//PApplet.println(connector); // prints the current
//com.sun.tools.jdi.AbstractLauncher al;
//com.sun.tools.jdi.RawCommandLineLauncher rcll;
//System.out.println(PApplet.javaVersion);
// http://java.sun.com/j2se/1.5.0/docs/guide/jpda/conninv.html#sunlaunch
try {
// boolean available = false;
// while (!available) {
while (true) {
try {
Messages.log(getClass().getName() + " attempting to attach to VM");
synchronized (cancelLock) {
vm = connector.attach(arguments);
if (cancelled && vm != null) {
// cancelled and connected to the VM, handle closing now
Messages.log(getClass().getName() + " aborting, launch cancelled");
close();
return false;
}
}
// vm = connector.attach(arguments);
if (vm != null) {
Messages.log(getClass().getName() + " attached to the VM");
// generateTrace();
// available = true;
return true;
}
} catch (ConnectException ce) {
// This will fire ConnectException (socket not available) until
// the VM finishes starting up and opens its socket for us.
Messages.log(getClass().getName() + " socket for VM not ready");
// System.out.println("waiting");
// e.printStackTrace();
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
Messages.loge(getClass().getName() + " interrupted", ie);
// ie.printStackTrace(sketchErr);
}
} catch (IOException e) {
Messages.loge(getClass().getName() + " while attaching to VM", e);
}
}
// } catch (IOException exc) {
// throw new Error("Unable to launch target VM: " + exc);
} catch (IllegalConnectorArgumentsException exc) {
throw new Error("Internal error: " + exc);
}
}
protected StringList getMachineParams() {
StringList params = new StringList();
//params.add("-Xint"); // interpreted mode
//params.add("-Xprof"); // profiler
//params.add("-Xaprof"); // allocation profiler
//params.add("-Xrunhprof:cpu=samples"); // old-style profiler
// TODO change this to use run.args = true, run.args.0, run.args.1, etc.
// so that spaces can be included in the arg names
String options = Preferences.get("run.options");
if (options.length() > 0) {
String pieces[] = PApplet.split(options, ' ');
for (int i = 0; i < pieces.length; i++) {
String p = pieces[i].trim();
if (p.length() > 0) {
params.append(p);
}
}
}
if (Preferences.getBoolean("run.options.memory")) {
params.append("-Xms" + Preferences.get("run.options.memory.initial") + "m");
params.append("-Xmx" + Preferences.get("run.options.memory.maximum") + "m");
}
// Surprised this wasn't here before; added for 3.2.1
params.append("-Djna.nosys=true");
// Added for 3.2.1, was still using the default ext.dirs in the PDE
try {
String extPath =
new File(Platform.getJavaHome(), "lib/ext").getCanonicalPath();
// quoting this on OS X causes it to fail
//params.append("-Djava.ext.dirs=\"" + extPath + "\"");
params.append("-Djava.ext.dirs=" + extPath);
} catch (IOException e) {
e.printStackTrace();
}
if (Platform.isMacOS()) {
// This successfully sets the application menu name,
// but somehow, not the dock name itself.
params.append("-Xdock:name=" + build.getSketchClassName());
// No longer needed / doesn't seem to do anything differently
//params.append("-Dcom.apple.mrj.application.apple.menu.about.name=" +
// build.getSketchClassName());
}
// sketch.libraryPath might be ""
// librariesClassPath will always have sep char prepended
params.append("-Djava.library.path=" +
build.getJavaLibraryPath() +
File.pathSeparator +
System.getProperty("java.library.path"));
params.append("-cp");
params.append(build.getClassPath());
// enable assertions
// http://dev.processing.org/bugs/show_bug.cgi?id=1188
params.append("-ea");
//PApplet.println(PApplet.split(sketch.classPath, ':'));
return params;
}
protected StringList getSketchParams(boolean present, String[] args) {
StringList params = new StringList();
// It's dangerous to add your own main() to your code,
// but if you've done it, we'll respect your right to hang yourself.
// http://processing.org/bugs/bugzilla/1446.html
if (build.getFoundMain()) {
params.append(build.getSketchClassName());
} else {
params.append("processing.core.PApplet");
// Get the stored device index (starts at 1)
// By default, set to -1, meaning 'the default display',
// which is the same display as the one being used by the Editor.
int runDisplay = Preferences.getInteger("run.display");
// If there was a saved location (this guy has been run more than once)
// then the location will be set to the last position of the sketch window.
// This will be passed to the PApplet runner using something like
// --location=30,20
// Otherwise, the editor location will be passed, and the applet will
// figure out where to place itself based on the editor location.
// --editor-location=150,20
if (editor != null) { // if running processing-cmd, don't do placement
GraphicsDevice editorDevice =
editor.getGraphicsConfiguration().getDevice();
GraphicsEnvironment ge =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] devices = ge.getScreenDevices();
// Make sure the display set in Preferences actually exists
GraphicsDevice runDevice = editorDevice;
if (runDisplay > 0 && runDisplay <= devices.length) {
runDevice = devices[runDisplay-1];
} else {
// If a bad display (or -1 display) is selected, use the same display as the editor
if (runDisplay > 0) { // don't complain about -1 or 0
System.err.println("Display " + runDisplay + " not available.");
}
runDevice = editorDevice;
for (int i = 0; i < devices.length; i++) {
if (devices[i] == runDevice) {
// Prevent message on the first run
if (runDisplay != -1) {
System.err.println("Setting 'Run Sketches on Display' preference to display " + (i+1));
}
runDisplay = i + 1;
// Wasn't setting the pref to avoid screwing things up with
// something temporary. But not setting it makes debugging one's
// setup just too damn weird, so changing that behavior.
Preferences.setInteger("run.display", runDisplay);
break;
}
}
}
Point windowLocation = editor.getSketchLocation();
// if (windowLocation != null) {
// // could check to make sure the sketch location is on the device
// // that's specified in Preferences, but that's going to be annoying
// // if you move a sketch to another window, then it keeps jumping
// // back to the specified window.
//// Rectangle screenRect =
//// runDevice.getDefaultConfiguration().getBounds();
// }
if (windowLocation == null) {
if (editorDevice == runDevice) {
// If sketches are to be shown on the same display as the editor,
// provide the editor location so the sketch's main() can place it.
Point editorLocation = editor.getLocation();
params.append(PApplet.ARGS_EDITOR_LOCATION + "=" +
editorLocation.x + "," + editorLocation.y);
} else {
// The sketch's main() will set a location centered on the new
// display. It has to happen in main() because the width/height
// of the sketch are not known here.
// Set a location centered on the other display
// Rectangle screenRect =
// runDevice.getDefaultConfiguration().getBounds();
// int runX =
// params.add(PApplet.ARGS_LOCATION + "=" + runX + "," + runY);
}
} else {
params.append(PApplet.ARGS_LOCATION + "=" +
windowLocation.x + "," + windowLocation.y);
}
params.append(PApplet.ARGS_EXTERNAL);
}
params.append(PApplet.ARGS_DISPLAY + "=" + runDisplay);
if (present) {
params.append(PApplet.ARGS_PRESENT);
// if (Preferences.getBoolean("run.present.exclusive")) {
// params.add(PApplet.ARGS_EXCLUSIVE);
// }
params.append(PApplet.ARGS_STOP_COLOR + "=" +
Preferences.get("run.present.stop.color"));
params.append(PApplet.ARGS_WINDOW_COLOR + "=" +
Preferences.get("run.present.bgcolor"));
}
// There was a PDE X hack that put this after the class name, but it was
// removed for 3.0a6 because it would break the args passed to sketches.
params.append(PApplet.ARGS_SKETCH_FOLDER + "=" + build.getSketchPath());
if (Toolkit.zoom(100) >= 200) { // Use 100 to bypass possible rounding in zoom()
params.append(PApplet.ARGS_DENSITY + "=2");
}
params.append(build.getSketchClassName());
}
// Add command-line arguments to be given to the sketch itself
if (args != null) {
params.append(args);
}
// Pass back the whole list
return params;
}
protected void launchJava(final String[] args) {
new Thread(new Runnable() {
public void run() {
// PApplet.println("java starting");
vmReturnedError = false;
process = PApplet.exec(args);
try {
// PApplet.println("java waiting");
int result = process.waitFor();
// PApplet.println("java done waiting");
if (result != 0) {
String[] errorStrings = PApplet.loadStrings(process.getErrorStream());
String[] inputStrings = PApplet.loadStrings(process.getInputStream());
// PApplet.println("launchJava stderr:");
// PApplet.println(errorStrings);
// PApplet.println("launchJava stdout:");
PApplet.printArray(inputStrings);
if (errorStrings != null && errorStrings.length > 1) {
if (errorStrings[0].indexOf("Invalid maximum heap size") != -1) {
Messages.showWarning("Way Too High",
"Please lower the value for \u201Cmaximum available memory\u201D in the\n" +
"Preferences window. For more information, read Help \u2192 Troubleshooting.", null);
} else {
for (String err : errorStrings) {
sketchErr.println(err);
}
sketchErr.println("Using startup command: " + PApplet.join(args, " "));
}
} else {
//exc.printStackTrace();
sketchErr.println("Could not run the sketch (Target VM failed to initialize).");
if (Preferences.getBoolean("run.options.memory")) {
// Only mention this if they've even altered the memory setup
sketchErr.println("Make sure that you haven't set the maximum available memory too high.");
}
sketchErr.println("For more information, read revisions.txt and Help \u2192 Troubleshooting.");
}
// changing this to separate editor and listener [091124]
//if (editor != null) {
listener.statusError("Could not run the sketch.");
vmReturnedError = true;
//}
// return null;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* Generate the trace.
* Enable events, start thread to display events,
* start threads to forward remote error and output streams,
* resume the remote VM, wait for the final event, and shutdown.
*/
protected void generateTrace() {
//vm.setDebugTraceMode(debugTraceMode);
// vm.setDebugTraceMode(VirtualMachine.TRACE_ALL);
// vm.setDebugTraceMode(VirtualMachine.TRACE_NONE); // formerly, seems to have no effect
try {
// Calling this seems to set something internally to make the
// Eclipse JDI wake up. Without it, an ObjectCollectedException
// is thrown on excReq.enable(). No idea why this works,
// but at least exception handling has returned. (Suspect that it may
// block until all or at least some threads are available, meaning
// that the app has launched and we have legit objects to talk to).
vm.allThreads();
// The bug may not have been noticed because the test suite waits for
// a thread to be available, and queries it by calling allThreads().
// See org.eclipse.debug.jdi.tests.AbstractJDITest for the example.
EventRequestManager mgr = vm.eventRequestManager();
// get only the uncaught exceptions
ExceptionRequest excReq = mgr.createExceptionRequest(null, false, true);
// this version reports all exceptions, caught or uncaught
// suspend so we can step
excReq.setSuspendPolicy(EventRequest.SUSPEND_ALL);
excReq.enable();
} catch (VMDisconnectedException ignore) {
return;
}
Thread eventThread = new Thread() {
public void run() {
try {
boolean connected = true;
while (connected) {
EventQueue eventQueue = vm.eventQueue();
// remove() blocks until event(s) available
EventSet eventSet = eventQueue.remove();
// listener.vmEvent(eventSet);
for (Event event : eventSet) {
// System.out.println("EventThread.handleEvent -> " + event);
if (event instanceof VMStartEvent) {
vm.resume();
} else if (event instanceof ExceptionEvent) {
// for (ThreadReference thread : vm.allThreads()) {
// System.out.println("thread : " + thread);
//// thread.suspend();
// }
exceptionEvent((ExceptionEvent) event);
} else if (event instanceof VMDisconnectEvent) {
connected = false;
}
}
}
// } catch (VMDisconnectedException e) {
// Logger.getLogger(VMEventReader.class.getName()).log(Level.INFO, "VMEventReader quit on VM disconnect");
} catch (Exception e) {
System.err.println("crashed in event thread due to " + e.getMessage());
// Logger.getLogger(VMEventReader.class.getName()).log(Level.SEVERE, "VMEventReader quit", e);
e.printStackTrace();
}
}
};
eventThread.start();
errThread =
new MessageSiphon(process.getErrorStream(), this).getThread();
outThread = new StreamRedirectThread("JVM stdout Reader",
process.getInputStream(),
sketchOut);
errThread.start();
outThread.start();
// Shutdown begins when event thread terminates
try {
if (eventThread != null) eventThread.join(); // is this the problem?
// System.out.println("in here");
// Bug #852 tracked to this next line in the code.
// http://dev.processing.org/bugs/show_bug.cgi?id=852
errThread.join(); // Make sure output is forwarded
// System.out.println("and then");
outThread.join(); // before we exit
// System.out.println("finished join for errThread and outThread");
// At this point, disable the run button.
// This happens when the sketch is exited by hitting ESC,
// or the user manually closes the sketch window.
// TODO this should be handled better, should it not?
if (editor != null) {
java.awt.EventQueue.invokeLater(() -> {
editor.onRunnerExiting(Runner.this);
});
}
} catch (InterruptedException exc) {
// we don't interrupt
}
//System.out.println("and leaving");
}
protected Connector findConnector(String connectorName) {
// List connectors =
// com.sun.jdi.Bootstrap.virtualMachineManager().allConnectors();
List<Connector> connectors =
org.eclipse.jdi.Bootstrap.virtualMachineManager().allConnectors();
// // debug: code to list available connectors
// Iterator iter2 = connectors.iterator();
// while (iter2.hasNext()) {
// Connector connector = (Connector)iter2.next();
// System.out.println("connector name is " + connector.name());
// }
for (Object c : connectors) {
Connector connector = (Connector) c;
// System.out.println(connector.name());
// }
// Iterator iter = connectors.iterator();
// while (iter.hasNext()) {
// Connector connector = (Connector)iter.next();
if (connector.name().equals(connectorName)) {
return connector;
}
}
Messages.showError("Compiler Error",
"findConnector() failed to find " +
connectorName + " inside Runner", null);
return null; // Not reachable
}
public void exceptionEvent(ExceptionEvent event) {
ObjectReference or = event.exception();
ReferenceType rt = or.referenceType();
String exceptionName = rt.name();
//Field messageField = Throwable.class.getField("detailMessage");
Field messageField = rt.fieldByName("detailMessage");
// System.out.println("field " + messageField);
Value messageValue = or.getValue(messageField);
// System.out.println("mess val " + messageValue);
//"java.lang.ArrayIndexOutOfBoundsException"
int last = exceptionName.lastIndexOf('.');
String message = exceptionName.substring(last + 1);
if (messageValue != null) {
String messageStr = messageValue.toString();
if (messageStr.startsWith("\"")) {
messageStr = messageStr.substring(1, messageStr.length() - 1);
}
message += ": " + messageStr;
}
// System.out.println("mess type " + messageValue.type());
//StringReference messageReference = (StringReference) messageValue.type();
// First just report the exception and its placement
reportException(message, or, event.thread());
// Then try to pretty it up with a better message
handleCommonErrors(exceptionName, message, listener, sketchErr);
if (editor != null) {
java.awt.EventQueue.invokeLater(() -> {
editor.onRunnerExiting(Runner.this);
});
}
}
/**
* Provide more useful explanations of common error messages, perhaps with
* a short message in the status area, and (if necessary) a longer message
* in the console.
*
* @param exceptionClass Class name causing the error (with full package name)
* @param message The message from the exception
* @param listener The Editor or command line interface that's listening for errors
* @return true if the error was purtified, false otherwise
*/
public static boolean handleCommonErrors(final String exceptionClass,
final String message,
final RunnerListener listener,
final PrintStream err) {
if (exceptionClass.equals("java.lang.OutOfMemoryError")) {
if (message.contains("exceeds VM budget")) {
// TODO this is a kludge for Android, since there's no memory preference
listener.statusError("OutOfMemoryError: This code attempts to use more memory than available.");
err.println("An OutOfMemoryError means that your code is either using up too much memory");
err.println("because of a bug (e.g. creating an array that's too large, or unintentionally");
err.println("loading thousands of images), or simply that it's trying to use more memory");
err.println("than what is supported by the current device.");
} else {
listener.statusError("OutOfMemoryError: You may need to increase the memory setting in Preferences.");
err.println("An OutOfMemoryError means that your code is either using up too much memory");
err.println("because of a bug (e.g. creating an array that's too large, or unintentionally");
err.println("loading thousands of images), or that your sketch may need more memory to run.");
err.println("If your sketch uses a lot of memory (for instance if it loads a lot of data files)");
err.println("you can increase the memory available to your sketch using the Preferences window.");
}
} else if (exceptionClass.equals("java.lang.UnsatisfiedLinkError")) {
listener.statusError("A library used by this sketch is not installed properly.");
if (PApplet.platform == PConstants.LINUX) {
err.println(message);
}
err.println("A library relies on native code that's not available.");
err.println("Or only works properly when the sketch is run as a " +
((Platform.getNativeBits() == 32) ? "64-bit" : "32-bit") + " application.");
} else if (exceptionClass.equals("java.lang.StackOverflowError")) {
listener.statusError("StackOverflowError: This sketch is attempting too much recursion.");
err.println("A StackOverflowError means that you have a bug that's causing a function");
err.println("to be called recursively (it's calling itself and going in circles),");
err.println("or you're intentionally calling a recursive function too much,");
err.println("and your code should be rewritten in a more efficient manner.");
} else if (exceptionClass.equals("java.lang.UnsupportedClassVersionError")) {
listener.statusError("UnsupportedClassVersionError: A library is using code compiled with an unsupported version of Java.");
err.println("This version of Processing only supports libraries and JAR files compiled for Java 1.8 or earlier.");
err.println("A library used by this sketch was compiled for Java 1.9 or later, ");
err.println("and needs to be recompiled to be compatible with Java 1.8.");
} else if (exceptionClass.equals("java.lang.NoSuchMethodError") ||
exceptionClass.equals("java.lang.NoSuchFieldError")) {
listener.statusError(exceptionClass.substring(10) + ": " +
"You may be using a library that's incompatible " +
"with this version of Processing.");
} else {
return false;
}
return true;
}
// TODO: This may be called more than one time per error in the VM,
// presumably because exceptions might be wrapped inside others,
// and this will fire for both.
protected void reportException(String message, ObjectReference or, ThreadReference thread) {
listener.statusError(findException(message, or, thread));
}
/**
* Move through a list of stack frames, searching for references to code
* found in the current sketch. Return with a RunnerException that contains
* the location of the error, or if nothing is found, just return with a
* RunnerException that wraps the error message itself.
*/
protected SketchException findException(String message, ObjectReference or, ThreadReference thread) {
try {
// use to dump the stack for debugging
// for (StackFrame frame : thread.frames()) {
// System.out.println("frame: " + frame);
// }
List<StackFrame> frames = thread.frames();
for (StackFrame frame : frames) {
try {
Location location = frame.location();
String filename = null;
filename = location.sourceName();
int lineNumber = location.lineNumber() - 1;
SketchException rex =
build.placeException(message, filename, lineNumber);
if (rex != null) {
return rex;
}
} catch (AbsentInformationException e) {
// Any of the thread.blah() methods can throw an AbsentInformationEx
// if that bit of data is missing. If so, just write out the error
// message to the console.
//e.printStackTrace(); // not useful
exception = new SketchException(message);
exception.hideStackTrace();
listener.statusError(exception);
}
}
} catch (IncompatibleThreadStateException e) {
// This shouldn't happen, but if it does, print the exception in case
// it's something that needs to be debugged separately.
e.printStackTrace(sketchErr);
} catch (Exception e) {
// stack overflows seem to trip in frame.location() above
// ignore this case so that the actual error gets reported to the user
if ("StackOverflowError".equals(message) == false) {
e.printStackTrace(sketchErr);
}
}
// before giving up, try to extract from the throwable object itself
// since sometimes exceptions are re-thrown from a different context
try {
// assume object reference is Throwable, get stack trace
Method method = ((ClassType) or.referenceType()).concreteMethodByName("getStackTrace", "()[Ljava/lang/StackTraceElement;");
ArrayReference result = (ArrayReference) or.invokeMethod(thread, method, new ArrayList<Value>(), ObjectReference.INVOKE_SINGLE_THREADED);
// iterate through stack frames and pull filename and line number for each
for (Value val: result.getValues()) {
ObjectReference ref = (ObjectReference)val;
method = ((ClassType) ref.referenceType()).concreteMethodByName("getFileName", "()Ljava/lang/String;");
StringReference strref = (StringReference) ref.invokeMethod(thread, method, new ArrayList<Value>(), ObjectReference.INVOKE_SINGLE_THREADED);
String filename = strref == null ? "Unknown Source" : strref.value();
method = ((ClassType) ref.referenceType()).concreteMethodByName("getLineNumber", "()I");
IntegerValue intval = (IntegerValue) ref.invokeMethod(thread, method, new ArrayList<Value>(), ObjectReference.INVOKE_SINGLE_THREADED);
int lineNumber = intval.intValue() - 1;
SketchException rex =
build.placeException(message, filename, lineNumber);
if (rex != null) {
return rex;
}
}
// for (Method m : ((ClassType) or.referenceType()).allMethods()) {
// System.out.println(m + " | " + m.signature() + " | " + m.genericSignature());
// }
// Implemented for 2.0b9, writes a stack trace when there's an internal error inside core.
method = ((ClassType) or.referenceType()).concreteMethodByName("printStackTrace", "()V");
// System.err.println("got method " + method);
or.invokeMethod(thread, method, new ArrayList<Value>(), ObjectReference.INVOKE_SINGLE_THREADED);
} catch (Exception e) {
// stack overflows will make the exception handling above trip again
// ignore this case so that the actual error gets reported to the user
if ("StackOverflowError".equals(message) == false) {
e.printStackTrace(sketchErr);
}
}
// Give up, nothing found inside the pile of stack frames
SketchException rex = new SketchException(message);
// exception is being created /here/, so stack trace is not useful
rex.hideStackTrace();
return rex;
}
public void close() {
synchronized (cancelLock) {
cancelled = true;
// TODO make sure stop() has already been called to exit the sketch
// TODO actually kill off the vm here
if (vm != null) {
try {
vm.exit(0);
} catch (com.sun.jdi.VMDisconnectedException vmde) {
// if the vm has disconnected on its own, ignore message
//System.out.println("harmless disconnect " + vmde.getMessage());
// TODO shouldn't need to do this, need to do more cleanup
}
}
}
}
// made synchronized for 0087
// attempted to remove synchronized for 0136 to fix bug #775 (no luck tho)
// http://dev.processing.org/bugs/show_bug.cgi?id=775
synchronized public void message(String s) {
// System.out.println("M" + s.length() + ":" + s.trim()); // + "MMM" + s.length());
// this eats the CRLFs on the lines.. oops.. do it later
//if (s.trim().length() == 0) return;
// this is PApplet sending a message (via System.out.println)
// that signals that the applet has been quit.
if (s.indexOf(PApplet.EXTERNAL_STOP) == 0) {
//System.out.println("external: quit");
if (editor != null) {
// editor.internalCloseRunner(); // [091124]
// editor.handleStop(); // prior to 0192
java.awt.EventQueue.invokeLater(() -> {
editor.internalCloseRunner(); // 0192
});
}
return;
}
// this is the PApplet sending us a message that the applet
// is being moved to a new window location
if (s.indexOf(PApplet.EXTERNAL_MOVE) == 0) {
String nums = s.substring(s.indexOf(' ') + 1).trim();
int space = nums.indexOf(' ');
int left = Integer.parseInt(nums.substring(0, space));
int top = Integer.parseInt(nums.substring(space + 1));
// this is only fired when connected to an editor
editor.setSketchLocation(new Point(left, top));
//System.out.println("external: move to " + left + " " + top);
return;
}
// these are used for debugging, in case there are concerns
// that some errors aren't coming through properly
// if (s.length() > 2) {
// System.err.println(newMessage);
// System.err.println("message " + s.length() + ":" + s);
// }
// always shove out the message, since it might not fall under
// the same setup as we're expecting
sketchErr.print(s);
//System.err.println("[" + s.length() + "] " + s);
sketchErr.flush();
}
}
@@ -0,0 +1,386 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import processing.mode.java.pdex.JavaTextAreaPainter;
public class ColorControlBox {
public boolean visible;
ArrayList<Handle> handles;
ColorMode colorMode;
Color color;
boolean ilegalColor = false;
boolean isBW;
boolean isHex;
String drawContext;
// interface
int x, y, width, height;
JavaTextAreaPainter painter;
public ColorControlBox(String context, ColorMode mode,
ArrayList<Handle> handles) {
this.drawContext = context;
this.colorMode = mode;
this.handles = handles;
// add this box to the handles so they can update this color on change
for (Handle h : handles) {
h.setColorBox(this);
}
isBW = isGrayScale();
isHex = isHexColor();
color = getCurrentColor();
visible = Settings.alwaysShowColorBoxes;
}
public void initInterface(JavaTextAreaPainter textAreaPainter,
int x, int y, int w, int h) {
this.painter = textAreaPainter;
this.x = x;
this.y = y;
this.width = w;
this.height = h;
}
public void setPos(int x, int y) {
this.x = x;
this.y = y;
}
public void draw(Graphics2D g2d) {
if (!visible) {
return;
}
AffineTransform trans = g2d.getTransform();
g2d.translate(x, y);
// draw current color
g2d.setColor(color);
g2d.fillRoundRect(0, 0, width, height, 5, 5);
// draw black outline
g2d.setStroke(new BasicStroke(1));
g2d.setColor(Color.BLACK);
g2d.drawRoundRect(0, 0, width, height, 5, 5);
if (ilegalColor) {
g2d.setColor(Color.RED);
g2d.setStroke(new BasicStroke(2));
g2d.drawLine(width-3, 3, 3, height-3);
}
g2d.setTransform(trans);
}
public boolean isGrayScale() {
if (handles.size() <= 2) {
int value = handles.get(0).newValue.intValue();
if ((value&0xff000000) == 0) {
return true;
}
}
return false;
}
/**
* @return true if number is hex or webcolor
*/
private boolean isHexColor() {
if (handles.get(0).type == "hex" || handles.get(0).type == "webcolor") {
int value = handles.get(0).value.intValue();
if ((value & 0xff000000) != 0) {
return true;
}
}
return false;
}
public Color getCurrentColor()
{
try {
if (handles.size() == 1)
{
if (isBW) {
// treat as color(gray)
float gray = handles.get(0).newValue.floatValue();
return verifiedGrayColor(gray);
}
else {
// treat as color(argb)
int argb = handles.get(0).newValue.intValue();
return verifiedHexColor(argb);
}
}
else if (handles.size() == 2)
{
if (isBW) {
// color(gray, alpha)
float gray = handles.get(0).newValue.floatValue();
return verifiedGrayColor(gray);
}
else {
// treat as color(argb, a)
int argb = handles.get(0).newValue.intValue();
float a = handles.get(1).newValue.floatValue();
return verifiedHexColor(argb, a);
}
}
else if (handles.size() == 3)
{
// color(v1, v2, v3)
float v1 = handles.get(0).newValue.floatValue();
float v2 = handles.get(1).newValue.floatValue();
float v3 = handles.get(2).newValue.floatValue();
if (colorMode.modeType == ColorMode.RGB) {
return verifiedRGBColor(v1, v2, v3, colorMode.aMax);
}
else {
return verifiedHSBColor(v1, v2, v3, colorMode.aMax);
}
}
else if (handles.size() == 4)
{
// color(v1, v2, v3, alpha)
float v1 = handles.get(0).newValue.floatValue();
float v2 = handles.get(1).newValue.floatValue();
float v3 = handles.get(2).newValue.floatValue();
float a = handles.get(3).newValue.floatValue();
if (colorMode.modeType == ColorMode.RGB) {
return verifiedRGBColor(v1, v2, v3, a);
}
else {
return verifiedHSBColor(v1, v2, v3, a);
}
}
}
catch (Exception e) {
System.out.println("error parsing color value: " + e.toString());
ilegalColor = true;
return Color.WHITE;
}
// couldn't figure out this color, return WHITE color
ilegalColor = true;
return Color.WHITE;
}
private Color verifiedGrayColor(float gray)
{
if (gray < 0 || gray > colorMode.v1Max) {
return colorError();
}
ilegalColor = false;
gray = gray/colorMode.v1Max * 255;
return new Color((int)gray, (int)gray, (int)gray, 255);
}
private Color verifiedHexColor(int argb)
{
int r = (argb>>16)&0xff;
int g = (argb>>8)&0xff;
int b = (argb&0xff);
ilegalColor = false;
return new Color(r, g, b, 255);
}
private Color verifiedHexColor(int argb, float alpha)
{
int r = (argb>>16)&0xff;
int g = (argb>>8)&0xff;
int b = (argb&0xff);
ilegalColor = false;
return new Color(r, g, b, 255);
}
public Color verifiedRGBColor(float r, float g, float b, float a)
{
if (r < 0 || r > colorMode.v1Max ||
g < 0 || g > colorMode.v2Max ||
b < 0 || b > colorMode.v3Max) {
return colorError();
}
ilegalColor = false;
r = r/colorMode.v1Max * 255;
g = g/colorMode.v2Max * 255;
b = b/colorMode.v3Max * 255;
return new Color((int)r, (int)g, (int)b, 255);
}
public Color verifiedHSBColor(float h, float s, float b, float a)
{
if (h < 0 || h > colorMode.v1Max ||
s < 0 || s > colorMode.v2Max ||
b < 0 || b > colorMode.v3Max) {
return colorError();
}
ilegalColor = false;
Color c = Color.getHSBColor(h/colorMode.v1Max, s/colorMode.v2Max, b/colorMode.v3Max);
return new Color(c.getRed(), c.getGreen(), c.getBlue(), 255);
}
private Color colorError()
{
ilegalColor = true;
return Color.WHITE;
}
public void colorChanged()
{
color = getCurrentColor();
}
public int getTabIndex()
{
return handles.get(0).tabIndex;
}
public int getLine()
{
return handles.get(0).line;
}
public int getCharIndex()
{
int lastHandle = handles.size()-1;
return handles.get(lastHandle).newEndChar + 2;
}
/* Check if the point is in the box
*
*/
public boolean pick(int mx, int my)
{
if (!visible) {
return false;
}
if (mx>x && mx < x+width && my>y && my<y+height) {
return true;
}
return false;
}
/* Only show the color box if mouse is on the same line
*
* return true if there was change
*/
public boolean setMouseY(int my)
{
boolean change = false;
if (my>y && my<y+height) {
if (!visible) {
change = true;
}
visible = true;
}
else {
if (visible) {
change = true;
}
visible = false;
}
return change;
}
/* Update the color numbers with the new values that were selected
* in the color selector
*
* hue, saturation and brightness parameters are always 0-255
*/
public void selectorChanged(int hue, int saturation, int brightness)
{
if (isBW) {
// color(gray) or color(gray, alpha)
handles.get(0).setValue((float)hue/255*colorMode.v1Max);
}
else {
if (handles.size() == 1 || handles.size() == 2) {
// color(argb)
int prevVal = handles.get(0).newValue.intValue();
int prevAlpha = (prevVal>>24)&0xff;
Color c = Color.getHSBColor((float)hue/255, (float)saturation/255, (float)brightness/255);
int newVal = (prevAlpha<<24) | (c.getRed()<<16) | (c.getGreen()<<8) | (c.getBlue());
handles.get(0).setValue(newVal);
}
else if (handles.size() == 3 || handles.size() == 4) {
// color(v1, v2, v3) or color(v1, v2, v3, alpha)
if (colorMode.modeType == ColorMode.HSB) {
// HSB
float v1 = (float)hue/255 * colorMode.v1Max;
float v2 = (float)saturation/255 * colorMode.v2Max;
float v3 = (float)brightness/255 * colorMode.v3Max;
handles.get(0).setValue(v1);
handles.get(1).setValue(v2);
handles.get(2).setValue(v3);
}
else {
// RGB
Color c = Color.getHSBColor((float)hue/255, (float)saturation/255, (float)brightness/255);
handles.get(0).setValue((float)c.getRed()/255*colorMode.v1Max);
handles.get(1).setValue((float)c.getGreen()/255*colorMode.v2Max);
handles.get(2).setValue((float)c.getBlue()/255*colorMode.v3Max);
}
}
}
// update our own color
color = getCurrentColor();
// update code text painter so the user will see the changes
painter.updateCodeText();
painter.repaint();
}
public String toString() {
return handles.size() + " handles, color mode: " + colorMode.toString();
}
}
@@ -0,0 +1,108 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
public class ColorMode {
final static int RGB = 0;
final static int HSB = 1;
float v1Max, v2Max, v3Max, aMax;
int modeType;
boolean unrecognizedMode;
String drawContext;
public ColorMode(String context) {
this.drawContext = context;
modeType = RGB;
v1Max = 255;
v2Max = 255;
v3Max = 255;
aMax = 255;
unrecognizedMode = false;
}
public ColorMode(String context, int type,
float v1, float v2, float v3, float a) {
this.drawContext = context;
modeType = type;
v1Max = v1;
v2Max = v2;
v3Max = v3;
aMax = a;
unrecognizedMode = false;
}
static public ColorMode fromString(String context, String mode) {
try {
String[] elements = mode.split(",");
// determine the type of the color mode
int type = RGB;
if (elements[0].trim().equals("HSB")) {
type = HSB;
}
if (elements.length == 1) {
// colorMode in the form of colorMode(type)
return new ColorMode(context, type, 255, 255, 255, 255);
} else if (elements.length == 2) {
// colorMode in the form of colorMode(type, max)
float max = Float.parseFloat(elements[1].trim());
return new ColorMode(context, type, max, max, max, max);
} else if (elements.length == 4) {
// colorMode in the form of colorMode(type, max1, max2, max3)
float r = Float.parseFloat(elements[1].trim());
float g = Float.parseFloat(elements[2].trim());
float b = Float.parseFloat(elements[3].trim());
return new ColorMode(context, type, r, g, b, 255);
} else if (elements.length == 5) {
// colorMode in the form of colorMode(type, max1, max2, max3, maxA)
float r = Float.parseFloat(elements[1].trim());
float g = Float.parseFloat(elements[2].trim());
float b = Float.parseFloat(elements[3].trim());
float a = Float.parseFloat(elements[4].trim());
return new ColorMode(context, type, r, g, b, a);
}
} catch (Exception e) { }
// if we failed to parse this mode (uses variables etc..)
// we should still keep it so we'll know there is a mode declaration
// and we should mark it as unrecognizable
ColorMode newMode = new ColorMode(context);
newMode.unrecognizedMode = true;
return newMode;
}
public String toString() {
String type = (modeType == RGB) ? "RGB" : "HSB";
return "ColorMode: " + type +
": (" + v1Max + ", " + v2Max + ", " + v3Max + ", " + aMax + ")";
}
}
@@ -0,0 +1,50 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
import java.awt.Color;
public class ColorScheme {
private static ColorScheme instance = null;
public Color redStrokeColor;
public Color progressFillColor;
public Color progressEmptyColor;
public Color markerColor;
public Color whitePaneColor;
private ColorScheme() {
redStrokeColor = new Color(160, 20, 20); // dark red
progressEmptyColor = new Color(180, 180, 180, 200);
progressFillColor = new Color(0, 0, 0, 200);
markerColor = new Color(228, 200, 91, 127);
whitePaneColor = new Color(255, 255, 255, 120);
}
public static ColorScheme getInstance() {
if (instance == null) {
instance = new ColorScheme();
}
return instance;
}
}
@@ -0,0 +1,430 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class ColorSelector {
int hue, saturation, brightness;
public JFrame frame;
public ColorControlBox colorBox;
ColorSelectorBox selectorBox;
ColorSelectorSlider selectorSlider;
SelectorTopBar topBar;
public ColorSelector(ColorControlBox colorBox) {
this.colorBox = colorBox;
createFrame();
}
public void createFrame() {
frame = new JFrame();
frame.setBackground(Color.BLACK);
Box box = Box.createHorizontalBox();
box.setBackground(Color.BLACK);
selectorSlider = new ColorSelectorSlider();
if (!colorBox.isBW) {
selectorBox = new ColorSelectorBox();
box.add(selectorBox);
}
box.add(Box.createHorizontalGlue());
box.add(selectorSlider, BorderLayout.CENTER);
box.add(Box.createHorizontalGlue());
frame.getContentPane().add(box, BorderLayout.CENTER);
frame.pack();
frame.setResizable(false);
frame.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
}
public void show(int x, int y) {
frame.setLocation(x, y);
frame.setVisible(true);
frame.repaint();
}
public void hide() {
this.colorBox = null;
frame.setVisible(false);
}
public void refreshColor() {
if (!colorBox.ilegalColor) {
setColor(colorBox.color);
}
}
public void setColor(Color c) {
if (selectorBox != null) {
selectorBox.setToColor(c);
}
selectorSlider.setToColor(c);
repaintSelector();
}
public void satBrightChanged() {
repaintSelector();
}
public void hueChanged() {
if (selectorBox != null) {
selectorBox.renderBack();
}
repaintSelector();
}
public void repaintSelector() {
if (selectorBox != null) {
selectorBox.repaint();
}
selectorSlider.repaint();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
class ColorSelectorBox extends JComponent {
int lastX, lastY;
BufferedImage backImg;
ColorSelectorBox() {
if (!colorBox.ilegalColor) {
setToColor(colorBox.color);
}
renderBack();
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
updateMouse(e);
}
});
addMouseMotionListener(new MouseAdapter() {
public void mouseDragged(MouseEvent e) {
updateMouse(e);
}
});
}
public void paintComponent(Graphics g) {
g.drawImage(backImg, 0, 0, this);
Graphics2D g2 = (Graphics2D) g;
// otherwise the oval is hideous
// TODO make a proper hidpi version of all this
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(lastY < 128 ? Color.BLACK : Color.WHITE);
AffineTransform tx = g2.getTransform();
g2.translate(lastX, lastY);
//g2.drawOval(0, 0, 5, 5);
g2.drawOval(-3, -3, 6, 6);
g2.drawLine(-8, 0, -6, 0);
g2.drawLine(6, 0, 8, 0);
g2.drawLine(0, -8, 0, -6);
g2.drawLine(0, 6, 0, 8);
g2.setTransform(tx);
}
public void renderBack() {
int[] pixels = new int[256 * 256];
int index = 0;
for (int j = 0; j < 256; j++) {
for (int i = 0; i < 256; i++) {
pixels[index++] = // color(hue, i, 255-j);
Color.HSBtoRGB(hue / 255f, (i / 255f), (255-j)/255f);
}
}
backImg = new BufferedImage(256, 256, BufferedImage.TYPE_INT_RGB);
backImg.getRaster().setDataElements(0, 0, 256, 256, pixels);
}
public void setToColor(Color c) {
// set selector color
float hsb[] = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null);
saturation = (int) (hsb[1] * 255);
brightness = (int) (hsb[2] * 255);
lastX = saturation;
lastY = 255 - brightness;
}
void updateMouse(MouseEvent event) {
int mouseX = event.getX();
int mouseY = event.getY();
if (mouseX >= 0 && mouseX < 256 &&
mouseY >= 0 && mouseY < 256) {
lastX = mouseX;
lastY = mouseY;
updateColor();
}
}
void updateColor() {
saturation = lastX;
brightness = 255 - lastY;
satBrightChanged();
colorBox.selectorChanged(hue, saturation, brightness);
}
public Dimension getPreferredSize() {
return new Dimension(256, 256);
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
public Dimension getMaximumSize() {
return getPreferredSize();
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
class ColorSelectorSlider extends JComponent {
final int WIDE = 30;
BufferedImage backImg;
int lastY;
ColorSelectorSlider() {
// size(30, 255);
// noLoop();
// colorMode(HSB, 255, 255, 255);
// strokeWeight(1);
// noFill();
// loadPixels();
if (!colorBox.ilegalColor) {
setToColor(colorBox.color);
}
// draw the slider background
renderBack();
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
updateMouse(e);
}
});
addMouseMotionListener(new MouseAdapter() {
public void mouseDragged(MouseEvent e) {
updateMouse(e);
}
});
}
public void paintComponent(Graphics g) {
g.drawImage(backImg, 0, 0, this);
Graphics2D g2 = (Graphics2D) g;
// if (colorBox.isBW) {
// stroke(lastY<128 ? 0 : 255);
// }
// else {
// stroke(0);
// }
if (colorBox.isBW && lastY >= 128) {
g2.setColor(Color.WHITE);
} else {
g2.setColor(Color.BLACK);
}
AffineTransform tx = g2.getTransform();
g2.translate(0, lastY);
// draw left bracket
// beginShape();
// vertex(5, -2);
// vertex(1, -2);
// vertex(1, 2);
// vertex(5, 2);
// endShape();
g.drawRect(1, -2, 6, 4);
// draw middle lines
g.drawLine(13, 0, 17, 0);
g.drawLine(15, -2, 15, 2);
// draw right bracket
// beginShape();
// vertex(24, -2);
// vertex(28, -2);
// vertex(28, 2);
// vertex(24, 2);
// endShape();
g.drawRect(24, -2, 4, 4);
g2.setTransform(tx);
/*
if (colorBox.isBW) {
// stroke(255);
// rect(0, 0, 29, 254);
g.setColor(Color.WHITE);
g.drawRect(0, 0, WIDE, 255);
} else {
// stroke(0);
// line(0, 0, 0, 255);
// line(29, 0, 29, 255);
g.setColor(Color.BLACK);
g.drawLine(0, 0, 0, 255);
g.drawLine(29, 0, 29, 255);
}
*/
}
void renderBack() {
int[] pixels = new int[WIDE * 256];
int index = 0;
int argb = 0;
for (int j = 0; j < 256; j++) {
if (colorBox.isBW) {
int gray = 255 - j;
argb = 0xff000000 | (gray << 16) | (gray << 8) | gray;
} else {
// color(255-j, 255, 255);
argb = Color.HSBtoRGB((255 - j) / 255f, 1, 1);
}
for (int i = 0; i < WIDE; i++) {
pixels[index++] = argb;
}
}
backImg = new BufferedImage(WIDE, 256, BufferedImage.TYPE_INT_RGB);
backImg.getRaster().setDataElements(0, 0, WIDE, 256, pixels);
}
void setToColor(Color c) {
// set slider position
if (colorBox.isBW) {
hue = c.getRed();
} else {
float hsb[] = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null);
hue = (int)(hsb[0]*255);
}
lastY = 255 - hue;
}
void updateMouse(MouseEvent event) {
int mouseY = event.getY();
if (mouseY >= 0 && mouseY < 256) {
lastY = mouseY;
updateColor();
}
}
public void updateColor() {
hue = 255 - lastY;
hueChanged();
colorBox.selectorChanged(hue, saturation, brightness);
}
public Dimension getPreferredSize() {
return new Dimension(30, 255);
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
public Dimension getMaximumSize() {
return getPreferredSize();
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public class SelectorTopBar extends JComponent {
int barWidth;
int barHeight = 16;
public SelectorTopBar(int w) {
barWidth = w;
}
@Override
public void paintComponent(Graphics g) {
g.setColor(Color.GRAY);
Dimension size = getSize();
g.fillRect(0, 0, size.width, size.height);
}
public Dimension getPreferredSize() {
return new Dimension(barWidth, barHeight);
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
public Dimension getMaximumSize() {
return getPreferredSize();
}
}
}
@@ -0,0 +1,92 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.geom.AffineTransform;
public class HProgressBar {
int x, y, size, width;
int pos;
int lPolyX, rPolyX;
Polygon rightPoly, leftPoly;
public HProgressBar(int size, int width) {
this.size = size;
this.width = width;
x = 0;
y = 0;
setPos(0);
int xl[] = {0, 0, -(int)(size/1.5)};
int yl[] = {-(int)((float)size/3), (int)((float)size/3), 0};
leftPoly = new Polygon(xl, yl, 3);
int xr[] = {0, (int)(size/1.5), 0};
int yr[] = {-(int)((float)size/3), 0, (int)((float)size/3)};
rightPoly = new Polygon(xr, yr, 3);
}
public void setPos(int pos) {
this.pos = pos;
lPolyX = 0;
rPolyX = 0;
if (pos > 0) {
rPolyX = pos;
}
else if (pos < 0) {
lPolyX = pos;
}
}
public void setWidth(int width) {
this.width = width;
}
public void draw(Graphics2D g2d) {
AffineTransform trans = g2d.getTransform();
g2d.translate(x, y);
// draw white cover on text line
g2d.setColor(ColorScheme.getInstance().whitePaneColor);
g2d.fillRect(-200+lPolyX, -size, 200-lPolyX-width/2, size+1);
g2d.fillRect(width/2, -size, 200+rPolyX, size+1);
// draw left and right triangles and leading line
g2d.setColor(ColorScheme.getInstance().progressFillColor);
AffineTransform tmp = g2d.getTransform();
g2d.translate(-width/2 - 5 + lPolyX, -size/2);
g2d.fillRect(0, -1, -lPolyX, 2);
g2d.fillPolygon(leftPoly);
g2d.setTransform(tmp);
g2d.translate(width/2 + 5 + rPolyX, -size/2);
g2d.fillRect(-rPolyX, -1, rPolyX+1, 2);
g2d.fillPolygon(rightPoly);
g2d.setTransform(tmp);
g2d.setTransform(trans);
}
}
@@ -0,0 +1,298 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.Locale;
public class Handle {
public String type;
public String name;
public String strValue;
public String strNewValue;
public int varIndex;
public int startChar;
public int endChar;
public int newStartChar;
public int newEndChar;
public int line;
int tabIndex;
int decimalPlaces; // number of digits after the decimal point
float incValue;
java.lang.Number value, newValue;
String strDiff;
// connect with color control box
ColorControlBox colorBox;
// interface
int x, y, width, height;
int xCenter, xCurrent, xLast;
HProgressBar progBar = null;
String textFormat;
// the client that sends the changes
TweakClient tweakClient;
public Handle(String t, String n, int vi, String v, int ti, int l, int sc,
int ec, int dp) {
type = t;
name = n;
varIndex = vi;
strValue = v;
tabIndex = ti;
line = l;
startChar = sc;
endChar = ec;
decimalPlaces = dp;
incValue = (float) (1 / Math.pow(10, decimalPlaces));
if ("int".equals(type)) {
value = newValue = Integer.parseInt(strValue);
strNewValue = strValue;
textFormat = "%d";
} else if ("hex".equals(type)) {
Long val = Long.parseLong(strValue.substring(2, strValue.length()), 16);
value = newValue = val.intValue();
strNewValue = strValue;
textFormat = "0x%x";
} else if ("webcolor".equals(type)) {
Long val = Long.parseLong(strValue.substring(1, strValue.length()), 16);
val = val | 0xff000000;
value = newValue = val.intValue();
strNewValue = strValue;
textFormat = "#%06x";
} else if ("float".equals(type)) {
value = newValue = Float.parseFloat(strValue);
strNewValue = strValue;
textFormat = "%.0" + decimalPlaces + "f";
}
newStartChar = startChar;
newEndChar = endChar;
}
public void initInterface(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
// create drag ball
progBar = new HProgressBar(height, width);
}
public void setCenterX(int mx) {
xLast = xCurrent = xCenter = mx;
}
public void setCurrentX(int mx) {
xLast = xCurrent;
xCurrent = mx;
progBar.setPos(xCurrent - xCenter);
updateValue();
}
public void resetProgress() {
progBar.setPos(0);
}
public void updateValue() {
float change = getChange();
if ("int".equals(type)) {
if (newValue.intValue() + (int) change > Integer.MAX_VALUE ||
newValue.intValue() + (int) change < Integer.MIN_VALUE) {
change = 0;
return;
}
setValue(newValue.intValue() + (int) change);
} else if ("hex".equals(type)) {
setValue(newValue.intValue() + (int) change);
} else if ("webcolor".equals(type)) {
setValue(newValue.intValue() + (int) change);
} else if ("float".equals(type)) {
setValue(newValue.floatValue() + change);
}
updateColorBox();
}
public void setValue(Number value) {
if ("int".equals(type)) {
newValue = value.intValue();
strNewValue = String.format(Locale.US, textFormat, newValue.intValue());
} else if ("hex".equals(type)) {
newValue = value.intValue();
strNewValue = String.format(Locale.US, textFormat, newValue.intValue());
} else if ("webcolor".equals(type)) {
newValue = value.intValue();
// keep only RGB
int val = (newValue.intValue() & 0xffffff);
strNewValue = String.format(Locale.US, textFormat, val);
} else if ("float".equals(type)) {
BigDecimal bd = new BigDecimal(value.floatValue());
bd = bd.setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP);
newValue = bd.floatValue();
strNewValue = String.format(Locale.US, textFormat, newValue.floatValue());
}
// send new data to the server in the sketch
sendNewValue();
}
public void updateColorBox() {
if (colorBox != null) {
colorBox.colorChanged();
}
}
private float getChange() {
int pixels = xCurrent - xLast;
return pixels * incValue;
}
public void setPos(int nx, int ny) {
x = nx;
y = ny;
}
public void setWidth(int w) {
width = w;
progBar.setWidth(w);
}
public void draw(Graphics2D g2d, boolean hasFocus) {
AffineTransform prevTrans = g2d.getTransform();
g2d.translate(x, y);
// draw underline on the number
g2d.setColor(ColorScheme.getInstance().progressFillColor);
g2d.drawLine(0, 0, width, 0);
if (hasFocus) {
if (progBar != null) {
g2d.translate(width / 2, 2);
progBar.draw(g2d);
}
}
g2d.setTransform(prevTrans);
}
public boolean pick(int mx, int my) {
return pickText(mx, my);
}
public boolean pickText(int mx, int my) {
return (mx > x - 2 && mx < x + width + 2 && my > y - height && my < y);
}
public boolean valueChanged() {
if ("int".equals(type)) {
return (value.intValue() != newValue.intValue());
} else if ("hex".equals(type)) {
return (value.intValue() != newValue.intValue());
} else if ("webcolor".equals(type)) {
return (value.intValue() != newValue.intValue());
} else {
return (value.floatValue() != newValue.floatValue());
}
}
public void setColorBox(ColorControlBox box) {
colorBox = box;
}
public void setTweakClient(TweakClient client) {
tweakClient = client;
}
public void sendNewValue() {
int index = varIndex;
try {
if ("int".equals(type)) {
tweakClient.sendInt(index, newValue.intValue());
} else if ("hex".equals(type)) {
tweakClient.sendInt(index, newValue.intValue());
} else if ("webcolor".equals(type)) {
tweakClient.sendInt(index, newValue.intValue());
} else if ("float".equals(type)) {
tweakClient.sendFloat(index, newValue.floatValue());
}
} catch (Exception e) {
System.out.println("error sending new value!");
}
}
public String toString() {
return type + " " + name + " = " + strValue + " (tab: " + tabIndex
+ ", line: " + line + ", start: " + startChar + ", end: "
+ endChar + ")";
}
}
/*
* Used for sorting the handles by order of occurrence inside each tab
*/
class HandleComparator implements Comparator<Handle> {
public int compare(Handle handle1, Handle handle2) {
int tab = handle1.tabIndex - handle2.tabIndex;
if (tab != 0) {
return tab;
}
return handle1.startChar - handle2.startChar;
}
}
@@ -0,0 +1,26 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
public class Settings {
public static boolean alwaysShowColorBoxes = true;
}
@@ -0,0 +1,188 @@
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 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.mode.java.tweak;
import java.net.*;
import java.nio.ByteBuffer;
public class TweakClient {
private DatagramSocket socket;
private InetAddress address;
private boolean initialized;
private int sketchPort;
static final int VAR_INT = 0;
static final int VAR_FLOAT = 1;
static final int SHUTDOWN = 0xffffffff;
public TweakClient(int sketchPort) {
this.sketchPort = sketchPort;
try {
socket = new DatagramSocket();
// only local sketch is allowed
address = InetAddress.getByName("127.0.0.1");
initialized = true;
} catch (SocketException e) {
initialized = false;
} catch (UnknownHostException e) {
socket.close();
initialized = false;
} catch (SecurityException e) {
socket.close();
initialized = false;
}
}
public void shutdown() {
if (initialized) {
// send shutdown to the sketch
sendShutdown();
initialized = false;
}
}
public boolean sendInt(int index, int val) {
if (initialized) {
try {
byte[] buf = new byte[12];
ByteBuffer bb = ByteBuffer.wrap(buf);
bb.putInt(0, VAR_INT);
bb.putInt(4, index);
bb.putInt(8, val);
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, sketchPort);
socket.send(packet);
return true;
} catch (Exception e) { }
}
return false;
}
public boolean sendFloat(int index, float val) {
if (initialized) {
try {
byte[] buf = new byte[12];
ByteBuffer bb = ByteBuffer.wrap(buf);
bb.putInt(0, VAR_FLOAT);
bb.putInt(4, index);
bb.putFloat(8, val);
socket.send(new DatagramPacket(buf, buf.length, address, sketchPort));
return true;
} catch (Exception e) { }
}
return false;
}
public boolean sendShutdown() {
if (initialized) {
try {
byte[] buf = new byte[12];
ByteBuffer bb = ByteBuffer.wrap(buf);
bb.putInt(0, SHUTDOWN);
socket.send(new DatagramPacket(buf, buf.length, address, sketchPort));
return true;
} catch (Exception e) { }
}
return false;
}
static public String getServerCode(int listenPort,
boolean hasInts, boolean hasFloats) {
String serverCode = ""+
"class TweakModeServer extends Thread\n"+
"{\n"+
" protected DatagramSocket socket = null;\n"+
" protected boolean running = true;\n"+
" final int INT_VAR = 0;\n"+
" final int FLOAT_VAR = 1;\n"+
" final int SHUTDOWN = 0xffffffff;\n"+
" public TweakModeServer() {\n"+
" this(\"TweakModeServer\");\n"+
" }\n"+
" public TweakModeServer(String name) {\n"+
" super(name);\n"+
" }\n"+
" public void setup()\n"+
" {\n"+
" try {\n"+
" socket = new DatagramSocket("+listenPort+");\n"+
" socket.setSoTimeout(250);\n"+
" } catch (IOException e) {\n"+
" println(\"error: could not create TweakMode server socket\");\n"+
" }\n"+
" }\n"+
" public void run()\n"+
" {\n"+
" byte[] buf = new byte[256];\n"+
" while(running)\n"+
" {\n"+
" try {\n"+
" DatagramPacket packet = new DatagramPacket(buf, buf.length);\n"+
" socket.receive(packet);\n"+
" ByteBuffer bb = ByteBuffer.wrap(buf);\n"+
" int type = bb.getInt(0);\n"+
" int index = bb.getInt(4);\n";
if (hasInts) {
serverCode +=
" if (type == INT_VAR) {\n"+
" int val = bb.getInt(8);\n"+
" tweakmode_int[index] = val;\n"+
" }\n"+
" else ";
}
if (hasFloats) {
serverCode +=
" if (type == FLOAT_VAR) {\n"+
" float val = bb.getFloat(8);\n"+
" tweakmode_float[index] = val;\n"+
" }\n"+
" else";
}
serverCode +=
" if (type == SHUTDOWN) {\n"+
" running = false;\n"+
" }\n"+
" } catch (SocketTimeoutException e) {\n"+
" // nothing to do here just try receiving again\n"+
" } catch (Exception e) {\n"+
" }\n"+
" }\n"+
" socket.close();\n"+
" }\n"+
"}\n\n\n";
return serverCode;
}
}