mirror of
https://github.com/processing/processing4.git
synced 2026-01-29 03:11:08 +01:00
1369 lines
45 KiB
Java
1369 lines
45 KiB
Java
/* -*- 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<FPSAnimator> 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<Integer, CursorInfo> cursors = new HashMap<>();
|
|
static Map<Integer, String> 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);
|
|
});
|
|
}
|
|
}
|