/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2012-23 The Processing Foundation Copyright (c) 2004-12 Ben Fry and Casey Reas Copyright (c) 2001-04 Massachusetts Institute of Technology This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 2.1. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package processing.opengl; import java.awt.Component; import java.awt.EventQueue; import java.awt.FileDialog; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import javax.swing.ImageIcon; import com.jogamp.common.util.IOUtil; import com.jogamp.common.util.IOUtil.ClassResources; import com.jogamp.nativewindow.NativeSurface; import com.jogamp.nativewindow.ScalableSurface; import com.jogamp.nativewindow.util.Dimension; import com.jogamp.nativewindow.util.PixelFormat; import com.jogamp.nativewindow.util.PixelRectangle; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilities; import com.jogamp.opengl.GLEventListener; import com.jogamp.opengl.GLException; import com.jogamp.opengl.GLProfile; import com.jogamp.opengl.GLDrawableFactory; import com.jogamp.nativewindow.MutableGraphicsConfiguration; import com.jogamp.nativewindow.WindowClosingProtocol; import com.jogamp.newt.Display; import com.jogamp.newt.Display.PointerIcon; import com.jogamp.newt.NewtFactory; import com.jogamp.newt.Screen; import com.jogamp.newt.awt.NewtCanvasAWT; import com.jogamp.newt.opengl.GLWindow; import com.jogamp.opengl.util.FPSAnimator; import processing.core.PApplet; import processing.core.PConstants; import processing.core.PGraphics; import processing.core.PImage; import processing.core.PSurface; import processing.event.KeyEvent; import processing.event.MouseEvent; import processing.awt.PImageAWT; // have this removed by 4.0 final import processing.awt.ShimAWT; public class PSurfaceJOGL implements PSurface { /** Selected GL profile */ static public GLProfile profile; public PJOGL pgl; protected GLWindow window; protected FPSAnimator animator; protected Rectangle screenRect; private Thread drawExceptionHandler; protected PApplet sketch; protected PGraphics graphics; protected int sketchWidthRequested; protected int sketchHeightRequested; protected int sketchWidth; protected int sketchHeight; protected Display display; protected Screen screen; protected Rectangle displayRect; protected Throwable drawException; private final Object drawExceptionMutex = new Object(); protected NewtCanvasAWT canvas; protected static GLAutoDrawable sharedDrawable; protected static ArrayList animators = new ArrayList<>(); private final static Object sharedSyncMutex = new Object(); private Object syncMutex; protected int windowScaleFactor; protected float[] currentPixelScale = { 0, 0 }; public PSurfaceJOGL(PGraphics graphics) { this.graphics = graphics; this.pgl = (PJOGL) ((PGraphicsOpenGL) graphics).pgl; } public void initOffscreen(PApplet sketch) { this.sketch = sketch; sketchWidth = sketch.sketchWidth(); sketchHeight = sketch.sketchHeight(); if (window != null) { canvas = new NewtCanvasAWT(window); canvas.setBounds(0, 0, window.getWidth(), window.getHeight()); canvas.setFocusable(true); } } public void initFrame(PApplet sketch) { this.sketch = sketch; initIcons(); // For 4.0.2, swapped the order to do initGL() before initDisplay() // https://github.com/processing/processing4/issues/544 initGL(); initDisplay(); initWindow(); initListeners(); initAnimator(); } public Object getNative() { return window; } protected void initDisplay() { display = NewtFactory.createDisplay(null); display.addReference(); screen = NewtFactory.createScreen(display, 0); screen.addReference(); int displayNum = sketch.sketchDisplay(); displayRect = getDisplayBounds(displayNum); } // TODO This code is mostly copied from code found in PSurfaceAWT. // It should probably be merged to avoid divergence. [fry 211122] static protected Rectangle getDisplayBounds(int displayNum) { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice[] awtDevices = ge.getScreenDevices(); GraphicsDevice awtDisplayDevice = null; if (displayNum > 0) { // if -1, use the default device if (displayNum <= awtDevices.length) { awtDisplayDevice = awtDevices[displayNum-1]; } else { System.err.format("Display %d does not exist, " + "using the default display instead.%n", displayNum); for (int i = 0; i < awtDevices.length; i++) { System.err.format("Display %d is %s%n", i+1, awtDevices[i]); } } // } else if (0 < awtDevices.length) { // TODO this seems like a bad idea: in lots of situations [0] will *not* // be the default device. Not sure why this was added instead of // just using getDefaultScreenDevice() below. [fry 211122] // TODO Removing for 4.2, clean this code on next visit. [fry 230218] // awtDisplayDevice = awtDevices[0]; } if (awtDisplayDevice == null) { awtDisplayDevice = ge.getDefaultScreenDevice(); } //return awtDisplayDevice.getDefaultConfiguration().getBounds(); Rectangle bounds = awtDisplayDevice.getDefaultConfiguration().getBounds(); float uiScale = Float.parseFloat(System.getProperty("sun.java2d.uiScale", "1.0")); return new Rectangle((int) (uiScale * bounds.getX()), (int) (uiScale * bounds.getY()), (int) (uiScale * bounds.getWidth()), (int) (uiScale * bounds.getHeight())); } protected void initGL() { if (profile == null) { if (PJOGL.profile == 1) { try { profile = GLProfile.getGL2ES1(); } catch (GLException ex) { profile = GLProfile.getMaxFixedFunc(true); } } else if (PJOGL.profile == 2) { try { profile = GLProfile.getGL2ES2(); // workaround for https://jogamp.org/bugzilla/show_bug.cgi?id=1347 if (!profile.isHardwareRasterizer()) { GLProfile hardware = GLProfile.getMaxProgrammable(true); if (hardware.isGL2ES2()) { profile = hardware; } } } catch (GLException ex) { profile = GLProfile.getMaxProgrammable(true); } } else if (PJOGL.profile == 3) { try { profile = GLProfile.getGL2GL3(); } catch (GLException ex) { profile = GLProfile.getMaxProgrammable(true); } if (!profile.isGL3()) { PGraphics.showWarning("Requested profile GL3 but is not available, got: " + profile); } } else if (PJOGL.profile == 4) { try { profile = GLProfile.getGL4ES3(); } catch (GLException ex) { profile = GLProfile.getMaxProgrammable(true); } if (!profile.isGL4()) { PGraphics.showWarning("Requested profile GL4 but is not available, got: " + profile); } } else throw new RuntimeException(PGL.UNSUPPORTED_GLPROF_ERROR); } // Setting up the desired capabilities; GLCapabilities caps = new GLCapabilities(profile); caps.setAlphaBits(PGL.REQUESTED_ALPHA_BITS); caps.setDepthBits(PGL.REQUESTED_DEPTH_BITS); caps.setStencilBits(PGL.REQUESTED_STENCIL_BITS); // caps.setPBuffer(false); // caps.setFBO(false); // pgl.reqNumSamples = PGL.smoothToSamples(graphics.smooth); caps.setSampleBuffers(true); caps.setNumSamples(PGL.smoothToSamples(graphics.smooth)); caps.setBackgroundOpaque(true); caps.setOnscreen(true); pgl.setCaps(caps); if (sharedDrawable == null) { // Create a shared drawable to enable context sharing across multiple GL windows // https://jogamp.org/deployment/jogamp-next/javadoc/jogl/javadoc/com/jogamp/opengl/GLSharedContextSetter.html sharedDrawable = GLDrawableFactory.getFactory(profile).createDummyAutoDrawable(null, true, caps, null); sharedDrawable.display(); } } // To properly deal with synchronization when context sharing across multiple drawables (windows), we need a // synchronization mutex object to ensure that rendering of each frame completes in their respective animator thread: // https://jogamp.org/deployment/jogamp-next/javadoc/jogl/javadoc/com/jogamp/opengl/GLSharedContextSetter.html#synchronization private Object getSyncMutex(GLAutoDrawable drawable) { pgl.getGL(drawable); if (pgl.needSharedObjectSync()) { syncMutex = sharedSyncMutex; } else { if (syncMutex == null) { syncMutex = new Object(); } } return syncMutex; } protected void initWindow() { window = GLWindow.create(screen, pgl.getCaps()); // Make sure that we pass the window close through to exit(), otherwise // we're likely to have OpenGL try to shut down halfway through rendering // a frame. Particularly problematic for complex/slow apps. // https://github.com/processing/processing/issues/4690 window.setDefaultCloseOperation(WindowClosingProtocol.WindowClosingMode.DO_NOTHING_ON_CLOSE); // macOS pixel density is handled transparently by the OS windowScaleFactor = (PApplet.platform == PConstants.MACOS) ? 1 : sketch.pixelDensity; boolean spanDisplays = sketch.sketchDisplay() == PConstants.SPAN; screenRect = spanDisplays ? // TODO probably need to apply this to the spanning version [fry 230218] new Rectangle(screen.getX(), screen.getY(), screen.getWidth(), screen.getHeight()) : new Rectangle((int) displayRect.getX(), (int) displayRect.getY(), (int) displayRect.getWidth(), (int) displayRect.getHeight()); // new Rectangle((int) (uiScale * displayRect.getX()), // (int) (uiScale * displayRect.getY()), // (int) (uiScale * displayRect.getWidth()), // (int) (uiScale * displayRect.getHeight())); // Set the displayWidth/Height variables inside PApplet, so that they're // usable and can even be returned by the sketchWidth()/Height() methods. sketch.displayWidth = screenRect.width; sketch.displayHeight = screenRect.height; // Sometimes the window manager or OS will resize the window. // Keep track of the requested width/height to notify the user. sketchWidthRequested = sketch.sketchWidth(); sketchHeightRequested = sketch.sketchHeight(); sketchWidth = sketch.sketchWidth(); sketchHeight = sketch.sketchHeight(); boolean fullScreen = sketch.sketchFullScreen(); // Before 3.x, we would set the window to full screen when the requested // width/height was the size of the screen. But not everyone *wants* that // to be full screen (they might want to drag the window, or who knows), // so that code was removed because fullScreen() is easy to use instead. // https://github.com/processing/processing/issues/3545 if (fullScreen || spanDisplays) { // sketchWidth = (int) (uiScale * screenRect.width / windowScaleFactor); // sketchHeight = (int) (uiScale * screenRect.height / windowScaleFactor); sketchWidth = screenRect.width / windowScaleFactor; sketchHeight = screenRect.height / windowScaleFactor; } sketch.setSize(sketchWidth, sketchHeight); // https://jogamp.org/deployment/jogamp-next/javadoc/jogl/javadoc/com/jogamp/newt/opengl/GLWindow.html#setSurfaceScale(float%5B%5D) float surfaceScale = (graphics.is2X() && PApplet.platform == PConstants.MACOS) ? ScalableSurface.AUTOMAX_PIXELSCALE : ScalableSurface.IDENTITY_PIXELSCALE; window.setSurfaceScale(new float[] { surfaceScale, surfaceScale }); window.setSize(sketchWidth * windowScaleFactor, sketchHeight * windowScaleFactor); window.setResizable(false); setSize(sketchWidth, sketchHeight); if (fullScreen) { PApplet.hideMenuBar(); if (spanDisplays) { window.setFullscreen(screen.getMonitorDevices()); } else { window.setUndecorated(true); window.setTopLevelPosition((int) displayRect.getX(), (int) displayRect.getY()); window.setTopLevelSize((int) displayRect.getWidth(), (int) displayRect.getHeight()); } } window.setSharedAutoDrawable(sharedDrawable); } protected void initListeners() { window.addMouseListener(new NEWTMouseListener()); window.addKeyListener(new NEWTKeyListener()); window.addWindowListener(new NEWTWindowListener()); window.addGLEventListener(new DrawListener()); } protected void initAnimator() { if (PApplet.platform == PConstants.WINDOWS) { // Force Windows to keep timer resolution high by creating a dummy // thread that sleeps for a time that is not a multiple of 10 ms. // See section titled "Clocks and Timers on Windows" in this post: // https://web.archive.org/web/20160308031939/https://blogs.oracle.com/dholmes/entry/inside_the_hotspot_vm_clocks Thread highResTimerThread = new Thread(() -> { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException ignore) { } }, "HighResTimerThread"); highResTimerThread.setDaemon(true); highResTimerThread.start(); } animator = new FPSAnimator(window, 60); animators.add(animator); drawException = null; animator.setUncaughtExceptionHandler((animator, drawable, cause) -> { synchronized (drawExceptionMutex) { drawException = cause; drawExceptionMutex.notify(); } }); drawExceptionHandler = new Thread(() -> { synchronized (drawExceptionMutex) { try { while (drawException == null) { drawExceptionMutex.wait(); } Throwable cause = drawException.getCause(); if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else if (cause instanceof UnsatisfiedLinkError) { throw new UnsatisfiedLinkError(cause.getMessage()); } else if (cause == null) { throw new RuntimeException(drawException.getMessage()); } else { throw new RuntimeException(cause); } } catch (InterruptedException ignored) { } } }); drawExceptionHandler.start(); } @Override public void setTitle(final String title) { display.getEDTUtil().invoke(false, () -> window.setTitle(title)); } @Override public void setVisible(final boolean visible) { display.getEDTUtil().invoke(false, () -> window.setVisible(visible)); } @Override public void setResizable(final boolean resizable) { display.getEDTUtil().invoke(false, () -> window.setResizable(resizable)); } public void setIcon(PImage icon) { PGraphics.showWarning("Window icons for OpenGL sketches can only be set in settings()\n" + "using PJOGL.setIcon(filename)."); } @Override public void setAlwaysOnTop(final boolean always) { display.getEDTUtil().invoke(false, () -> window.setAlwaysOnTop(always)); } protected void initIcons() { IOUtil.ClassResources res; if (PJOGL.icons == null || PJOGL.icons.length == 0) { // Default Processing icons final int[] sizes = { 16, 32, 48, 64, 128, 256, 512 }; String[] iconImages = new String[sizes.length]; for (int i = 0; i < sizes.length; i++) { iconImages[i] = "/icon/icon-" + sizes[i] + ".png"; } res = new ClassResources(iconImages, PApplet.class.getClassLoader(), PApplet.class); } else { // Loading custom icons from user-provided files. String[] iconImages = new String[PJOGL.icons.length]; for (int i = 0; i < PJOGL.icons.length; i++) { iconImages[i] = resourceFilename(PJOGL.icons[i]); } res = new ClassResources(iconImages, sketch.getClass().getClassLoader(), sketch.getClass()); } NewtFactory.setWindowIcons(res); } private String resourceFilename(String filename) { // The code below comes from PApplet.createInputRaw() with a few adaptations InputStream stream; try { // First see if it's in a data folder. This may fail by throwing // a SecurityException. If so, this whole block will be skipped. File file = new File(sketch.dataPath(filename)); if (!file.exists()) { // next see if it's just in the sketch folder file = sketch.sketchFile(filename); } if (file.exists() && !file.isDirectory()) { try { // handle case sensitivity check String filePath = file.getCanonicalPath(); String filenameActual = new File(filePath).getName(); // make sure there isn't a subfolder prepended to the name String filenameShort = new File(filename).getName(); // if the actual filename is the same, but capitalized // differently, warn the user. //if (filenameActual.equalsIgnoreCase(filenameShort) && //!filenameActual.equals(filenameShort)) { if (!filenameActual.equals(filenameShort)) { throw new RuntimeException("This file is named " + filenameActual + " not " + filename + ". Rename the file " + "or change your code."); } } catch (IOException ignored) { } } stream = new FileInputStream(file); stream.close(); return file.getCanonicalPath(); // have to break these out because a general Exception might // catch the RuntimeException being thrown above } catch (IOException | SecurityException ignored) { } ClassLoader cl = sketch.getClass().getClassLoader(); try { // by default, data files are exported to the root path of the jar. // (not the data folder) so check there first. stream = cl.getResourceAsStream("data/" + filename); if (stream != null) { String cn = stream.getClass().getName(); // this is an irritation of sun's java plug-in, which will return // a non-null stream for an object that doesn't exist. like all good // things, this is probably introduced in java 1.5. awesome! // https://download.processing.org/bugzilla/359.html if (!cn.equals("sun.plugin.cache.EmptyInputStream")) { stream.close(); return "data/" + filename; } } // When used with an online script, also need to check without the // data folder, in case it's not in a subfolder called 'data'. // https://download.processing.org/bugzilla/389.html stream = cl.getResourceAsStream(filename); if (stream != null) { String cn = stream.getClass().getName(); if (!cn.equals("sun.plugin.cache.EmptyInputStream")) { stream.close(); return filename; } } } catch (IOException ignored) { } try { // attempt to load from a local file try { String path = sketch.dataPath(filename); stream = new FileInputStream(path); stream.close(); return path; } catch (IOException ignored) { } try { String path = sketch.sketchPath(filename); stream = new FileInputStream(path); stream.close(); return path; } catch (Exception ignored) { } try { stream = new FileInputStream(filename); stream.close(); return filename; } catch (IOException ignored) { } } catch (Exception e) { //die(e.getMessage(), e); e.printStackTrace(); } return ""; } @Override public void placeWindow(int[] location, int[] editorLocation) { if (sketch.sketchFullScreen()) { return; } int x = window.getX() - window.getInsets().getLeftWidth(); int y = window.getY() - window.getInsets().getTopHeight(); int w = window.getWidth() + window.getInsets().getTotalWidth(); int h = window.getHeight() + window.getInsets().getTotalHeight(); if (location != null) { window.setTopLevelPosition(location[0], location[1]); } else if (editorLocation != null) { int locationX = editorLocation[0] - 20; int locationY = editorLocation[1]; if (locationX - w > 10) { // if it fits to the left of the window window.setTopLevelPosition(locationX - w, locationY); } else { // doesn't fit locationX = (sketch.displayWidth - w) / 2; locationY = (sketch.displayHeight - h) / 2; window.setTopLevelPosition(locationX, locationY); } } else { // just center on screen // Can't use frame.setLocationRelativeTo(null) because it sends the // frame to the main display, which undermines the --display setting. window.setTopLevelPosition(screenRect.x + (screenRect.width - sketchWidth) / 2, screenRect.y + (screenRect.height - sketchHeight) / 2); } Point frameLoc = new Point(x, y); if (frameLoc.y < 0) { // Windows actually allows you to place frames where they can't be // closed. Awesome. https://download.processing.org/bugzilla/1508.html window.setTopLevelPosition(frameLoc.x, 30); } } public void placePresent(int stopColor) { float scale = getPixelScale(); pgl.initPresentMode(0.5f * (screenRect.width/scale - sketchWidth), 0.5f * (screenRect.height/scale - sketchHeight), stopColor); PApplet.hideMenuBar(); window.setUndecorated(true); window.setTopLevelPosition((int) displayRect.getX(), (int) displayRect.getY()); window.setTopLevelSize((int) displayRect.getWidth(), (int) displayRect.getHeight()); } /* public void setupExternalMessages() { external = true; } */ public void startThread() { if (animator != null) { animator.start(); } } public void pauseThread() { if (animator != null) { animator.pause(); } } public void resumeThread() { if (animator != null) { animator.resume(); } } public boolean stopThread() { if (drawExceptionHandler != null) { drawExceptionHandler.interrupt(); drawExceptionHandler = null; } if (animator != null) { // Stops all other animators to avoid exceptions when closing a window in a multiple window configuration for (FPSAnimator ani: animators) { if (ani != animator) ani.stop(); } return animator.stop(); } else { return false; } } public boolean isStopped() { if (animator != null) { return !animator.isAnimating(); } else { return true; } } public void setLocation(final int x, final int y) { display.getEDTUtil().invoke(false, () -> window.setTopLevelPosition(x, y)); } public void setSize(int wide, int high) { if (pgl.presentMode()) return; // When the surface is set to resizable via surface.setResizable(true), // a crash may occur if the user sets the window to size zero. // https://github.com/processing/processing/issues/5052 if (high <= 0) { high = 1; } if (wide <= 0) { wide = 1; } boolean changed = sketch.width != wide || sketch.height != high; sketchWidth = wide; sketchHeight = high; sketch.setSize(wide, high); graphics.setSize(wide, high); if (changed) { window.setSize(wide * windowScaleFactor, high * windowScaleFactor); } } public float getPixelScale() { if (graphics.pixelDensity == 1) { return 1; } if (PApplet.platform == PConstants.MACOS) { return getCurrentPixelScale(); } return 2; } protected float getCurrentPixelScale() { // Even if the graphics are retina, the user might have moved the window // into a non-retina monitor, so we need to check return window.getCurrentSurfaceScale(currentPixelScale)[0]; } public Component getComponent() { return canvas; } public void setSmooth(int level) { pgl.reqNumSamples = level; GLCapabilities caps = new GLCapabilities(profile); caps.setAlphaBits(PGL.REQUESTED_ALPHA_BITS); caps.setDepthBits(PGL.REQUESTED_DEPTH_BITS); caps.setStencilBits(PGL.REQUESTED_STENCIL_BITS); caps.setSampleBuffers(true); caps.setNumSamples(pgl.reqNumSamples); caps.setBackgroundOpaque(true); caps.setOnscreen(true); NativeSurface target = window.getNativeSurface(); MutableGraphicsConfiguration config = (MutableGraphicsConfiguration) target.getGraphicsConfiguration(); config.setChosenCapabilities(caps); } public void setFrameRate(float fps) { if (fps < 1) { PGraphics.showWarning( "The OpenGL renderer cannot have a frame rate lower than 1.\n" + "Your sketch will run at 1 frame per second."); fps = 1; } else if (fps > 1000) { PGraphics.showWarning( "The OpenGL renderer cannot have a frame rate higher than 1000.\n" + "Your sketch will run at 1000 frames per second."); fps = 1000; } if (animator != null) { animator.stop(); animator.setFPS((int)fps); pgl.setFps(fps); animator.start(); } } public void requestFocus() { display.getEDTUtil().invoke(false, () -> window.requestFocus()); } public class DrawListener implements GLEventListener { private boolean isInit = false; public void display(GLAutoDrawable drawable) { if (!isInit) return; if (display.getEDTUtil().isCurrentThreadEDT()) { // For some unknown reason, a few frames of the animator run on // the EDT. For those, we just skip this draw call to avoid badness. // See below for explanation of this two line hack. pgl.beginRender(); pgl.endRender(sketch.sketchWindowColor()); return; } if (sketch.frameCount == 0) { if (sketchWidth != sketchWidthRequested || sketchHeight != sketchHeightRequested) { if (!sketch.sketchFullScreen()) { // don't show the message when using fullScreen() PGraphics.showWarning( "The sketch has been resized from " + "%dx%d to %dx%d by the operating system.%n" + "This happened outside Processing, " + "and may be a limitation of the OS or window manager.", sketchWidthRequested, sketchHeightRequested, sketchWidth, sketchHeight); } } requestFocus(); } if (!sketch.finished) { synchronized (getSyncMutex(drawable)) { pgl.getGL(drawable); int prevFrameCount = sketch.frameCount; sketch.handleDraw(); if (prevFrameCount == sketch.frameCount || sketch.finished) { // This hack allows the FBO layer to be swapped normally even if // the sketch is no looping or finished because it does not call draw(), // otherwise background artifacts may occur (depending on the hardware/drivers). pgl.beginRender(); pgl.endRender(sketch.sketchWindowColor()); } PGraphicsOpenGL.completeFinishedPixelTransfers(); } } if (sketch.exitCalled()) { PGraphicsOpenGL.completeAllPixelTransfers(); sketch.dispose(); // calls stopThread(), which stops the animator. sketch.exitActual(); } } public void dispose(GLAutoDrawable drawable) { // do nothing, sketch.dispose() will be called with exitCalled() } public void init(GLAutoDrawable drawable) { if (display.getEDTUtil().isCurrentThreadEDT()) { return; } synchronized (getSyncMutex(drawable)) { pgl.init(drawable); sketch.start(); int c = graphics.backgroundColor; pgl.clearColor(((c >> 16) & 0xff) / 255f, ((c >> 8) & 0xff) / 255f, (c & 0xff) / 255f, ((c >> 24) & 0xff) / 255f); pgl.clear(PGL.COLOR_BUFFER_BIT); isInit = true; } } public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) { if (!isInit) return; if (display.getEDTUtil().isCurrentThreadEDT()) { return; } synchronized (getSyncMutex(drawable)) { pgl.resetFBOLayer(); float scale = PApplet.platform == PConstants.MACOS ? getCurrentPixelScale() : getPixelScale(); setSize((int) (w / scale), (int) (h / scale)); } } } protected class NEWTWindowListener implements com.jogamp.newt.event.WindowListener { public NEWTWindowListener() { super(); } @Override public void windowGainedFocus(com.jogamp.newt.event.WindowEvent arg0) { sketch.focused = true; sketch.focusGained(); } @Override public void windowLostFocus(com.jogamp.newt.event.WindowEvent arg0) { sketch.focused = false; sketch.focusLost(); } @Override public void windowDestroyNotify(com.jogamp.newt.event.WindowEvent arg0) { sketch.exit(); } @Override public void windowDestroyed(com.jogamp.newt.event.WindowEvent arg0) { sketch.exit(); } @Override public void windowMoved(com.jogamp.newt.event.WindowEvent arg0) { /* if (external) { sketch.frameMoved(window.getX(), window.getY()); } */ sketch.postWindowMoved(window.getX(), window.getY()); } @Override public void windowRepaint(com.jogamp.newt.event.WindowUpdateEvent arg0) { } @Override public void windowResized(com.jogamp.newt.event.WindowEvent arg0) { sketch.postWindowResized(window.getWidth(), window.getHeight()); } } // NEWT mouse listener protected class NEWTMouseListener extends com.jogamp.newt.event.MouseAdapter { public NEWTMouseListener() { super(); } @Override public void mousePressed(com.jogamp.newt.event.MouseEvent e) { nativeMouseEvent(e, MouseEvent.PRESS); } @Override public void mouseReleased(com.jogamp.newt.event.MouseEvent e) { nativeMouseEvent(e, MouseEvent.RELEASE); } @Override public void mouseClicked(com.jogamp.newt.event.MouseEvent e) { nativeMouseEvent(e, MouseEvent.CLICK); } @Override public void mouseDragged(com.jogamp.newt.event.MouseEvent e) { nativeMouseEvent(e, MouseEvent.DRAG); } @Override public void mouseMoved(com.jogamp.newt.event.MouseEvent e) { nativeMouseEvent(e, MouseEvent.MOVE); } @Override public void mouseWheelMoved(com.jogamp.newt.event.MouseEvent e) { nativeMouseEvent(e, MouseEvent.WHEEL); } @Override public void mouseEntered(com.jogamp.newt.event.MouseEvent e) { // System.out.println("enter"); nativeMouseEvent(e, MouseEvent.ENTER); } @Override public void mouseExited(com.jogamp.newt.event.MouseEvent e) { // System.out.println("exit"); nativeMouseEvent(e, MouseEvent.EXIT); } } // NEWT key listener protected class NEWTKeyListener extends com.jogamp.newt.event.KeyAdapter { public NEWTKeyListener() { super(); } @Override public void keyPressed(com.jogamp.newt.event.KeyEvent e) { nativeKeyEvent(e, KeyEvent.PRESS); } @Override public void keyReleased(com.jogamp.newt.event.KeyEvent e) { nativeKeyEvent(e, KeyEvent.RELEASE); } public void keyTyped(com.jogamp.newt.event.KeyEvent e) { nativeKeyEvent(e, KeyEvent.TYPE); } } protected void nativeMouseEvent(com.jogamp.newt.event.MouseEvent nativeEvent, int peAction) { // SHIFT, CTRL, META, and ALT are identical to the processing.event.Event, // so the modifiers are left intact here. int modifiers = nativeEvent.getModifiers(); // Could limit to just the specific modifiers, but why bother? /* int peModifiers = modifiers & (InputEvent.SHIFT_MASK | InputEvent.CTRL_MASK | InputEvent.META_MASK | InputEvent.ALT_MASK); */ int peButton = switch (nativeEvent.getButton()) { case com.jogamp.newt.event.MouseEvent.BUTTON1 -> PConstants.LEFT; case com.jogamp.newt.event.MouseEvent.BUTTON2 -> PConstants.CENTER; case com.jogamp.newt.event.MouseEvent.BUTTON3 -> PConstants.RIGHT; default -> 0; }; int peCount; if (peAction == MouseEvent.WHEEL) { // Invert wheel rotation count so it matches JAVA2D's // https://github.com/processing/processing/issues/3840 peCount = -(nativeEvent.isShiftDown() ? (int)nativeEvent.getRotation()[0]: (int)nativeEvent.getRotation()[1]); } else { peCount = nativeEvent.getClickCount(); } int scale; if (PApplet.platform == PConstants.MACOS) { scale = (int) getCurrentPixelScale(); } else { scale = (int) getPixelScale(); } int sx = nativeEvent.getX() / scale; int sy = nativeEvent.getY() / scale; int mx = sx; int my = sy; if (pgl.presentMode()) { mx -= (int)pgl.presentX; my -= (int)pgl.presentY; //noinspection IntegerDivisionInFloatingPointContext if (peAction == KeyEvent.RELEASE && pgl.insideStopButton(sx, sy - screenRect.height / windowScaleFactor)) { sketch.exit(); } if (mx < 0 || sketchWidth < mx || my < 0 || sketchHeight < my) { return; } } MouseEvent me = new MouseEvent(nativeEvent, nativeEvent.getWhen(), peAction, modifiers, mx, my, peButton, peCount); sketch.postEvent(me); } private short normalizeKeyCode(short newtKeyCode) { switch (newtKeyCode) { case com.jogamp.newt.event.KeyEvent.VK_PAGE_UP: return java.awt.event.KeyEvent.VK_PAGE_UP; case com.jogamp.newt.event.KeyEvent.VK_SHIFT: return java.awt.event.KeyEvent.VK_SHIFT; default: return newtKeyCode; } } protected void nativeKeyEvent(com.jogamp.newt.event.KeyEvent nativeEvent, int peAction) { // SHIFT, CTRL, META, and ALT are identical to processing.event.Event int modifiers = nativeEvent.getModifiers(); // int peModifiers = nativeEvent.getModifiers() & // (InputEvent.SHIFT_MASK | // InputEvent.CTRL_MASK | // InputEvent.META_MASK | // InputEvent.ALT_MASK); short code = normalizeKeyCode(nativeEvent.getKeyCode()); char keyChar; int keyCode; if (isPCodedKey(code, nativeEvent.isPrintableKey())) { keyCode = mapToPConst(code); keyChar = PConstants.CODED; } else if (isHackyKey(code)) { // we can return only one char for ENTER, let it be \n everywhere keyCode = code == com.jogamp.newt.event.KeyEvent.VK_ENTER ? PConstants.ENTER : code; keyChar = hackToChar(code, nativeEvent.getKeyChar()); } else { keyCode = code; keyChar = nativeEvent.getKeyChar(); } // From http://jogamp.org/deployment/v2.1.0/javadoc/jogl/javadoc/com/jogamp/newt/event/KeyEvent.html // public final short getKeySymbol() // Returns the virtual key symbol reflecting the current keyboard layout. // public final short getKeyCode() // Returns the virtual key code using a fixed mapping to the US keyboard layout. // In contrast to key symbol, key code uses a fixed US keyboard layout and therefore is keyboard layout independent. // E.g. virtual key code VK_Y denotes the same physical key regardless whether keyboard layout QWERTY or QWERTZ is active. The key symbol of the former is VK_Y, where the latter produces VK_Y. KeyEvent ke = new KeyEvent(nativeEvent, nativeEvent.getWhen(), peAction, modifiers, keyChar, keyCode, nativeEvent.isAutoRepeat()); sketch.postEvent(ke); if (!isPCodedKey(code, nativeEvent.isPrintableKey()) && !isHackyKey(code)) { if (peAction == KeyEvent.PRESS) { // Create key typed event // TODO: combine dead keys with the following key KeyEvent tke = new KeyEvent(nativeEvent, nativeEvent.getWhen(), KeyEvent.TYPE, modifiers, keyChar, 0, nativeEvent.isAutoRepeat()); sketch.postEvent(tke); } } } private static boolean isPCodedKey(short code, boolean printable) { return code == com.jogamp.newt.event.KeyEvent.VK_UP || code == com.jogamp.newt.event.KeyEvent.VK_DOWN || code == com.jogamp.newt.event.KeyEvent.VK_LEFT || code == com.jogamp.newt.event.KeyEvent.VK_RIGHT || code == com.jogamp.newt.event.KeyEvent.VK_ALT || code == com.jogamp.newt.event.KeyEvent.VK_CONTROL || code == com.jogamp.newt.event.KeyEvent.VK_SHIFT || code == com.jogamp.newt.event.KeyEvent.VK_WINDOWS || (!printable && !isHackyKey(code)); } // Why do we need this mapping? // Relevant discussion and links here: // http://forum.jogamp.org/Newt-wrong-keycode-for-key-td4033690.html#a4033697 // (I don't think this is a complete solution). private static int mapToPConst(short code) { return switch (code) { case com.jogamp.newt.event.KeyEvent.VK_UP -> PConstants.UP; case com.jogamp.newt.event.KeyEvent.VK_DOWN -> PConstants.DOWN; case com.jogamp.newt.event.KeyEvent.VK_LEFT -> PConstants.LEFT; case com.jogamp.newt.event.KeyEvent.VK_RIGHT -> PConstants.RIGHT; case com.jogamp.newt.event.KeyEvent.VK_ALT -> PConstants.ALT; case com.jogamp.newt.event.KeyEvent.VK_CONTROL -> PConstants.CONTROL; case com.jogamp.newt.event.KeyEvent.VK_SHIFT -> PConstants.SHIFT; case com.jogamp.newt.event.KeyEvent.VK_WINDOWS -> java.awt.event.KeyEvent.VK_META; default -> code; }; } private static boolean isHackyKey(short code) { return (code == com.jogamp.newt.event.KeyEvent.VK_BACK_SPACE || code == com.jogamp.newt.event.KeyEvent.VK_TAB || code == com.jogamp.newt.event.KeyEvent.VK_ENTER || code == com.jogamp.newt.event.KeyEvent.VK_ESCAPE || code == com.jogamp.newt.event.KeyEvent.VK_DELETE); } private static char hackToChar(short code, char def) { return switch (code) { case com.jogamp.newt.event.KeyEvent.VK_BACK_SPACE -> PConstants.BACKSPACE; case com.jogamp.newt.event.KeyEvent.VK_TAB -> PConstants.TAB; case com.jogamp.newt.event.KeyEvent.VK_ENTER -> PConstants.ENTER; case com.jogamp.newt.event.KeyEvent.VK_ESCAPE -> PConstants.ESC; case com.jogamp.newt.event.KeyEvent.VK_DELETE -> PConstants.DELETE; default -> def; }; } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . // CURSORS class CursorInfo { PImage image; int x, y; CursorInfo(PImage image, int x, int y) { this.image = image; this.x = x; this.y = y; } void set() { setCursor(image, x, y); } } static Map cursors = new HashMap<>(); static Map cursorNames = new HashMap<>(); static { cursorNames.put(PConstants.ARROW, "arrow"); cursorNames.put(PConstants.CROSS, "cross"); cursorNames.put(PConstants.WAIT, "wait"); cursorNames.put(PConstants.MOVE, "move"); cursorNames.put(PConstants.HAND, "hand"); cursorNames.put(PConstants.TEXT, "text"); } public void setCursor(int kind) { if (!cursorNames.containsKey(kind)) { PGraphics.showWarning("Unknown cursor type: " + kind); return; } CursorInfo cursor = cursors.get(kind); if (cursor == null) { String name = cursorNames.get(kind); if (name != null) { URL url = getClass().getResource("cursors/" + name + ".png"); if (url != null) { ImageIcon icon = new ImageIcon(url); PImage img = new PImageAWT(icon.getImage()); // Most cursors just use the center as the hotspot... int x = img.width / 2; int y = img.height / 2; // ...others are more specific if (kind == PConstants.ARROW) { x = 10; y = 7; } else if (kind == PConstants.HAND) { x = 12; y = 8; } else if (kind == PConstants.TEXT) { x = 16; y = 22; } cursor = new CursorInfo(img, x, y); cursors.put(kind, cursor); } } } if (cursor != null) { cursor.set(); } else { PGraphics.showWarning("Cannot load cursor type: " + kind); } } public void setCursor(PImage image, int hotspotX, int hotspotY) { // TODO why is this first getting 'display' from 'window' instead of using // this.display which is already set? In addition, this.display is // even used to call getEDTUtil() down below. [fry 211123] Display display = window.getScreen().getDisplay(); BufferedImage img = (BufferedImage) image.getNative(); int[] imagePixels = ((DataBufferInt) img.getData().getDataBuffer()).getData(); ByteBuffer pixels = ByteBuffer.allocate(imagePixels.length * 4); pixels.asIntBuffer().put(imagePixels); PixelFormat format = PixelFormat.ARGB8888; final Dimension size = new Dimension(img.getWidth(), img.getHeight()); PixelRectangle rect = new PixelRectangle.GenericPixelRect(format, size, 0, false, pixels); final PointerIcon pi = display.createPointerIcon(rect, hotspotX, hotspotY); this.display.getEDTUtil().invoke(false, () -> { window.setPointerVisible(true); window.setPointerIcon(pi); }); } public void showCursor() { display.getEDTUtil().invoke(false, () -> window.setPointerVisible(true)); } public void hideCursor() { display.getEDTUtil().invoke(false, () -> window.setPointerVisible(false)); } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . // TOOLKIT @Override public PImage loadImage(String path, Object... args) { // Would like to rewrite this for 4.x, but the strategies for loading // image data with GL seem unnecessarily complex, and not 100% necessary: // we haven't had to remove as much AWT as expected. [fry 211123] return ShimAWT.loadImage(sketch, path, args); } @Override public boolean openLink(String url) { return ShimAWT.openLink(url); } @Override public void selectInput(String prompt, String callbackMethod, File file, Object callbackObject) { EventQueue.invokeLater(() -> { // https://github.com/processing/processing/issues/3831 boolean hide = (sketch != null) && (PApplet.platform == PConstants.WINDOWS); if (hide) setVisible(false); ShimAWT.selectImpl(prompt, callbackMethod, file, callbackObject, null, FileDialog.LOAD); if (hide) setVisible(true); }); } @Override public void selectOutput(String prompt, String callbackMethod, File file, Object callbackObject) { EventQueue.invokeLater(() -> { // https://github.com/processing/processing/issues/3831 boolean hide = (sketch != null) && (PApplet.platform == PConstants.WINDOWS); if (hide) setVisible(false); ShimAWT.selectImpl(prompt, callbackMethod, file, callbackObject, null, FileDialog.SAVE); if (hide) setVisible(true); }); } @Override public void selectFolder(String prompt, String callbackMethod, File file, Object callbackObject) { EventQueue.invokeLater(() -> { // https://github.com/processing/processing/issues/3831 boolean hide = (sketch != null) && (PApplet.platform == PConstants.WINDOWS); if (hide) setVisible(false); ShimAWT.selectFolderImpl(prompt, callbackMethod, file, callbackObject, null); if (hide) setVisible(true); }); } }