package rprocessing; import java.awt.Component; import java.awt.Window; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.lang.Thread.UncaughtExceptionHandler; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import javax.script.ScriptException; import org.renjin.parser.RParser; import org.renjin.sexp.Closure; import org.renjin.sexp.ExpressionVector; import org.renjin.sexp.FunctionCall; import org.renjin.sexp.SEXP; import org.renjin.sexp.Symbol; import com.jogamp.newt.opengl.GLWindow; import processing.awt.PSurfaceAWT; import processing.core.PApplet; import processing.core.PConstants; import processing.core.PSurface; import processing.event.KeyEvent; import processing.event.MouseEvent; import processing.javafx.PSurfaceFX; import processing.opengl.PSurfaceJOGL; import rprocessing.applet.BuiltinApplet; import rprocessing.exception.NotFoundException; import rprocessing.exception.RSketchError; import rprocessing.util.Constant; import rprocessing.util.Printer; import rprocessing.util.RScriptReader; /** * RlangPApplet PApplet for R language, powered by Renjin. * * @author github.com/gaocegege */ public class RLangPApplet extends BuiltinApplet { private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("VERBOSE_RLANG_MODE")); // A static-mode sketch must be interpreted from within the setup() method. // All others are interpreted during construction in order to harvest method // definitions, which we then invoke during the run loop. private final Mode mode; /** Program code */ private final String programText; private ExpressionVector expressionVector; private static final String CORE_TEXT = RScriptReader.readResourceAsText(Runner.class, "r/core.R"); private final Printer stdout; private final CountDownLatch finishedLatch = new CountDownLatch(1); private RSketchError terminalException = null; private boolean hasSize = false; private SEXP sizeFunction = null; /** * Mode for Processing. * * @author github.com/gaocegege */ private enum Mode { STATIC, ACTIVE, MIXED } private static void log(String msg) { if (!VERBOSE) { return; } System.err.println(RLangPApplet.class.getSimpleName() + ": " + msg); } public RLangPApplet(final String programText, final Printer stdout) throws NotFoundException { this.programText = programText; this.stdout = stdout; this.prePassCode(); // Detect the mode after pre-pass program code. this.mode = this.detectMode(); } public void evaluateCoreCode() throws RSketchError { try { this.renjinEngine.eval(CORE_TEXT); } catch (final ScriptException se) { throw RSketchError.toSketchException(se); } } /** * Evaluate all the function calls. */ public void prePassCode() { SEXP source = RParser.parseSource(this.programText + "\n", "inline-string"); if (isSameClass(source, ExpressionVector.class)) { ExpressionVector ev = (ExpressionVector) source; // Stores the expressions except size(). List<SEXP> sexps = new ArrayList<SEXP>(); for (int i = ev.length() - 1; i >= 0; --i) { if (isSameClass(ev.get(i), FunctionCall.class) && isSameClass(((FunctionCall) ev.get(i)).getFunction(), Symbol.class)) { if (((Symbol) ((FunctionCall) ev.get(i)).getFunction()).getPrintName().equals("<-")) { this.renjinEngine.getTopLevelContext().evaluate(ev.get(i), this.renjinEngine.getTopLevelContext().getEnvironment()); sexps.add(ev.get(i)); } else if (((Symbol) ((FunctionCall) ev.get(i)).getFunction()).getPrintName() .equals(Constant.SIZE_NAME)) { // size function is defined in global namespace. log("size function is defined in global namespace."); hasSize = true; sizeFunction = ev.get(i); } else { sexps.add(ev.get(i)); } } } expressionVector = new ExpressionVector(sexps); } } /** * Detect the mode. After: prePassCode() */ private Mode detectMode() { if (isActiveMode()) { if (isMixMode()) { return Mode.MIXED; } return Mode.ACTIVE; } return Mode.STATIC; } /** * Add PApplet instance to R top context Notice: DO NOT do it in constructor. */ public void addPAppletToRContext() { this.renjinEngine.put(Constant.PROCESSING_VAR_NAME, this); // This is a trick to be deprecated. It is used to print // messages in Processing app console by stdout$print(msg). this.renjinEngine.put("stdout", stdout); this.renjinEngine.put("key", "0"); this.renjinEngine.put("keyCode", 0); } public void runBlock(final String[] arguments) throws RSketchError { log("runBlock"); PApplet.runSketch(arguments, this); try { finishedLatch.await(); log("RunSketch done."); } catch (final InterruptedException interrupted) { // Treat an interruption as a request to the applet to terminate. exit(); try { finishedLatch.await(); log("RunSketch interrupted."); } catch (final InterruptedException exception) { log(exception.toString()); } } finally { Thread.setDefaultUncaughtExceptionHandler(null); if (PApplet.platform == PConstants.MACOSX && Arrays.asList(arguments).contains("fullScreen")) { // Frame should be OS-X fullscreen, and it won't stop being that unless the jvm // exits or we explicitly tell it to minimize. // (If it's disposed, it'll leave a gray blank window behind it.) log("Disabling fullscreen."); macosxFullScreenToggle(frame); } if (surface instanceof PSurfaceFX) { // Sadly, JavaFX is an abomination, and there's no way to run an FX sketch more than once, // so we must actually exit. log("JavaFX requires SketchRunner to terminate. Farewell!"); System.exit(0); } final Object nativeWindow = surface.getNative(); if (nativeWindow instanceof com.jogamp.newt.Window) { ((com.jogamp.newt.Window) nativeWindow).destroy(); } else { surface.setVisible(false); } } // log(terminalException.toString()); if (terminalException != null) { log("Throw the exception to PDE."); throw terminalException; } } private static void macosxFullScreenToggle(final Window window) { try { final Class<?> appClass = Class.forName("com.apple.eawt.Application"); final Method getAppMethod = appClass.getMethod("getApplication"); final Object app = getAppMethod.invoke(null); final Method requestMethod = appClass.getMethod("requestToggleFullScreen", Window.class); requestMethod.invoke(app, window); } catch (final ClassNotFoundException cnfe) { // ignored } catch (final Exception exception) { exception.printStackTrace(); } } /** * * @see processing.core.PApplet#initSurface() */ @Override protected PSurface initSurface() { final PSurface s = super.initSurface(); this.frame = null; // eliminate a memory leak from 2.x compat hack // s.setTitle(pySketchPath.getFileName().toString().replaceAll("\\..*$", "")); if (s instanceof PSurfaceAWT) { final PSurfaceAWT surf = (PSurfaceAWT) s; final Component c = (Component) surf.getNative(); c.addComponentListener(new ComponentAdapter() { @Override public void componentHidden(final ComponentEvent e) { log("initSurface"); finishedLatch.countDown(); } }); } else if (s instanceof PSurfaceJOGL) { final PSurfaceJOGL surf = (PSurfaceJOGL) s; final GLWindow win = (GLWindow) surf.getNative(); win.addWindowListener(new com.jogamp.newt.event.WindowAdapter() { @Override public void windowDestroyed(final com.jogamp.newt.event.WindowEvent arg0) { log("initSurface"); finishedLatch.countDown(); } }); } else if (s instanceof PSurfaceFX) { System.err.println("I don't know how to watch FX2D windows for close."); } return s; } @Override public void exitActual() { log("exitActual"); finishedLatch.countDown(); } /** * @see processing.core.PApplet#start() */ @Override public void start() { // I want to quit on runtime exceptions. // Processing just sits there by default. Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(final Thread t, final Throwable e) { terminalException = RSketchError.toSketchException(e); try { log("There is an unexpected exception."); handleMethods("dispose"); } catch (final Exception noop) { // give up } finishedLatch.countDown(); } }); super.start(); } /** * @see processing.core.PApplet#settings() */ @Override public void settings() { if (mode == Mode.MIXED || mode == Mode.STATIC) { this.renjinEngine.getTopLevelContext().evaluate(this.sizeFunction, this.renjinEngine.getTopLevelContext().getEnvironment()); } applyFunction(Constant.SETTINGS_NAME); } /** * Evaluate the program code. * * @see processing.core.PApplet#setup() */ @Override public void setup() { // I don't know why I put it there. Now I think it should be in constructor. // But I ... wrapProcessingVariables(); if (this.mode == Mode.STATIC) { try { log("The mode is static, run the program directly."); // The code includes size function but it would not raise a error, I don't know what happens // although it works well. this.renjinEngine.eval(this.programText); log("Evaluate the code in static mode."); } catch (final Exception exception) { log("There is exception when evaluate the code in static mode."); log(exception.toString()); terminalException = RSketchError.toSketchException(exception); exitActual(); } } else if (this.mode == Mode.ACTIVE) { Object obj = this.renjinEngine.get(Constant.SETUP_NAME); if (obj.getClass().equals(Closure.class)) { ((Closure) obj).doApply(this.renjinEngine.getTopLevelContext()); } } else { System.out.println("The program is in mix mode now."); applyFunction(Constant.SETUP_NAME); } log("Setup done"); } @Override public void handleDraw() { super.handleDraw(); this.wrapFrameVariables(); } /** * Call the draw function in R script. * * @see processing.core.PApplet#draw() */ @Override public void draw() { applyFunction(Constant.DRAW_NAME); } /* * Helper functions */ /** * Detect whether the program is in active mode. * * @return */ @SuppressWarnings("rawtypes") private boolean isActiveMode() { Class closureClass = Closure.class; return isSameClass(this.renjinEngine.get(Constant.SETTINGS_NAME), closureClass) || isSameClass(this.renjinEngine.get(Constant.SETUP_NAME), closureClass) || isSameClass(this.renjinEngine.get(Constant.DRAW_NAME), closureClass); } /** * Detect whether the program is in mix mode. After: isActiveMode() * * @return */ private boolean isMixMode() { return hasSize; } protected void wrapFrameVariables() { this.renjinEngine.put("frameRateVar", frameRate); this.renjinEngine.put("frameCount", frameCount); } /** * Set Environment variables in R top context. */ protected void wrapProcessingVariables() { log("Wrap Processing built-in variables into R top context."); this.wrapMouseVariables(); this.wrapKeyVariables(); this.renjinEngine.put("width", width); this.renjinEngine.put("height", height); this.renjinEngine.put("displayWidth", displayWidth); this.renjinEngine.put("displayHeight", displayHeight); this.renjinEngine.put("focused", focused); this.renjinEngine.put("pixelWidth", pixelWidth); this.renjinEngine.put("pixelHeight", pixelHeight); } @Override protected void handleMouseEvent(MouseEvent event) { super.handleMouseEvent(event); wrapMouseVariables(); } @Override public void mouseClicked() { wrapMouseVariables(); applyFunction(Constant.MOUSECLICKED_NAME); } @Override public void mouseMoved() { wrapMouseVariables(); applyFunction(Constant.MOUSEMOVED_NAME); } @Override public void mousePressed() { wrapMouseVariables(); applyFunction(Constant.MOUSEPRESSED_NAME); } @Override public void mouseReleased() { wrapMouseVariables(); applyFunction(Constant.MOUSERELEASED_NAME); } @Override public void mouseDragged() { wrapMouseVariables(); applyFunction(Constant.MOUSEDRAGGED_NAME); } /** * * @see processing.core.PApplet#focusGained() */ @Override public void focusGained() { super.focusGained(); this.renjinEngine.put("focused", super.focused); } /** * * @see processing.core.PApplet#focusLost() */ @Override public void focusLost() { super.focusLost(); this.renjinEngine.put("focused", super.focused); } private void wrapMouseVariables() { this.renjinEngine.put("mouseX", mouseX); this.renjinEngine.put("mouseY", mouseY); this.renjinEngine.put("pmouseX", pmouseX); this.renjinEngine.put("pmouseY", pmouseY); this.renjinEngine.put("mouseButtonVar", mouseButton); this.renjinEngine.put("mousePressedVar", mousePressed); } private void applyFunction(String name) { Object obj = this.renjinEngine.get(name); if (obj.getClass().equals(Closure.class)) { ((Closure) obj).doApply(this.renjinEngine.getTopLevelContext()); } } @Override protected void handleKeyEvent(KeyEvent event) { super.handleKeyEvent(event); wrapKeyVariables(); } @Override public void keyPressed() { wrapKeyVariables(); applyFunction(Constant.KEYPRESSED_NAME); } @Override public void keyReleased() { wrapKeyVariables(); applyFunction(Constant.KEYRELEASED_NAME); } @Override public void keyTyped() { wrapKeyVariables(); applyFunction(Constant.KEYTYPED_NAME); } protected void wrapKeyVariables() { this.renjinEngine.put("key", String.valueOf(key)); this.renjinEngine.put("keyCode", keyCode); this.renjinEngine.put("keyPressedVar", keyPressed); } /** * Return whether the object has same class with clazz. * * @param obj * @param clazz * @return */ @SuppressWarnings("rawtypes") private static boolean isSameClass(Object obj, Class clazz) { return obj.getClass().equals(clazz); } }