package processing.awt; import processing.core.PApplet; import processing.core.PConstants; import processing.core.PImage; import javax.imageio.*; import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.swing.*; import javax.swing.filechooser.FileSystemView; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.geom.AffineTransform; import java.awt.image.*; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.function.Consumer; /** * This class exists as an abstraction layer to remove AWT from PApplet. * It is a staging area for AWT-specific code that's shared by the Java2D, * JavaFX, and JOGL renderers. Once PSurfaceFX and PSurfaceJOGL have * their own implementations, these methods will move to PSurfaceAWT. */ public class ShimAWT implements PConstants { /* PGraphics graphics; PApplet sketch; public ShimAWT(PApplet sketch) { this.graphics = graphics; this.sketch = sketch; } */ static private ShimAWT instance; final private GraphicsDevice[] displayDevices; final private int displayWidth; final private int displayHeight; /** Only needed for display functions */ static private ShimAWT getInstance() { if (instance == null) { instance = new ShimAWT(); } return instance; } private ShimAWT() { // Need the list of display devices to be queried already for usage below. // https://github.com/processing/processing/issues/3295 // https://github.com/processing/processing/issues/3296 // Not doing this from a static initializer because it may cause // PApplet to cache and the values to stick through subsequent runs. // Instead make it a runtime thing and a local variable. GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice device = ge.getDefaultScreenDevice(); displayDevices = ge.getScreenDevices(); // // Default or unparsed will be -1, spanning will be 0, actual displays will // // be numbered from 1 because it's too weird to say "display 0" in prefs. // if (display > 0 && display <= displayDevices.length) { // device = displayDevices[display-1]; // } // When this was called, display will always be unset (even in 3.x), // since this happens before settings() is called. // Set displayWidth and displayHeight for people still using those. DisplayMode displayMode = device.getDisplayMode(); displayWidth = displayMode.getWidth(); displayHeight = displayMode.getHeight(); } static public int getDisplayWidth() { return getInstance().displayWidth; } static public int getDisplayHeight() { return getInstance().displayHeight; } static public int getDisplayCount() { return getInstance().displayDevices.length; } static public int getDisplayDensity(int num) { return getInstance().displayDensityImpl(num); } /* private int displayDensityImpl() { if (display != SPAN && (fullScreen || present)) { return displayDensity(display); } // walk through all displays, use 2 if any display is 2 for (int i = 0; i < displayDevices.length; i++) { if (displayDensity(i+1) == 2) { return 2; } } // If nobody's density is 2 then everyone is 1 return 1; } */ private int displayDensityImpl(int display) { GraphicsConfiguration graphicsConfig = null; if (display == -1) { // the default display GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); graphicsConfig = ge.getDefaultScreenDevice().getDefaultConfiguration(); } else if (display == SPAN) { // walk through all displays, go with lowest common denominator for (int i = 0; i < displayDevices.length; i++) { if (displayDensityImpl(i) == 1) { return 1; } } return 2; // everyone is density 2 } else if (display <= displayDevices.length) { graphicsConfig = displayDevices[display - 1].getDefaultConfiguration(); } if (graphicsConfig == null) { System.err.println("Display " + display + " does not exist, " + "returning 1 for displayDensity(" + display + ")"); return 1; // not the end of the world, so don't throw a RuntimeException } else { AffineTransform tx = graphicsConfig.getDefaultTransform(); return (int) Math.round(tx.getScaleX()); } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . static public void fromNativeImage(Image img, PImage out) { out.format = RGB; out.pixels = null; if (img instanceof BufferedImage) { BufferedImage bi = (BufferedImage) img; out.width = bi.getWidth(); out.height = bi.getHeight(); int type = bi.getType(); if (type == BufferedImage.TYPE_3BYTE_BGR || type == BufferedImage.TYPE_4BYTE_ABGR) { out.pixels = new int[out.width * out.height]; bi.getRGB(0, 0, out.width, out.height, out.pixels, 0, out.width); if (type == BufferedImage.TYPE_4BYTE_ABGR) { out.format = ARGB; // } else { // opaque(); } } else { DataBuffer db = bi.getRaster().getDataBuffer(); if (db instanceof DataBufferInt) { out.pixels = ((DataBufferInt) db).getData(); if (type == BufferedImage.TYPE_INT_ARGB) { out.format = ARGB; // } else if (type == BufferedImage.TYPE_INT_RGB) { // opaque(); } } } } if ((out.pixels != null) && (out.format == RGB)) { for (int i = 0; i < out.pixels.length; i++) { out.pixels[i] |= 0xFF000000; } } // Implements fall-through if not DataBufferInt above, or not a // known type, or not DataBufferInt for the data itself. if (out.pixels == null) { // go the old school Java 1.0 route out.width = img.getWidth(null); out.height = img.getHeight(null); out.pixels = new int[out.width * out.height]; PixelGrabber pg = new PixelGrabber(img, 0, 0, out.width, out.height, out.pixels, 0, out.width); try { pg.grabPixels(); } catch (InterruptedException ignored) { } } out.pixelDensity = 1; out.pixelWidth = out.width; out.pixelHeight = out.height; } // /** Set the high bits of all pixels to opaque. */ // protected void opaque() { // for (int i = 0; i < pixels.length; i++) { // pixels[i] = 0xFF000000 | pixels[i]; // } // } static public Object getNativeImage(PImage img) { img.loadPixels(); int type = (img.format == RGB) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; BufferedImage image = new BufferedImage(img.pixelWidth, img.pixelHeight, type); WritableRaster wr = image.getRaster(); wr.setDataElements(0, 0, img.pixelWidth, img.pixelHeight, img.pixels); return image; } static public void resizeImage(PImage img, int w, int h, int interpolationMode) { // ignore if (w <= 0 && h <= 0) { throw new IllegalArgumentException("width or height must be > 0 for resize"); } if (w == 0) { // Use height to determine relative size float diff = (float) h / (float) img.height; w = (int) (img.width * diff); } else if (h == 0) { // Use the width to determine relative size float diff = (float) w / (float) img.width; h = (int) (img.height * diff); } BufferedImage bimg = shrinkImage((BufferedImage) img.getNative(), w*img.pixelDensity, h*img.pixelDensity, interpolationMode); PImage temp = new PImageAWT(bimg); img.pixelWidth = temp.width; img.pixelHeight = temp.height; // Get the resized pixel array img.pixels = temp.pixels; img.width = img.pixelWidth / img.pixelDensity; img.height = img.pixelHeight / img.pixelDensity; // Mark the pixels array as altered img.updatePixels(); } // Adapted from getFasterScaledInstance() method from page 111 of // "Filthy Rich Clients" by Chet Haase and Romain Guy // Additional modifications and simplifications have been added, // plus a fix to deal with an infinite loop if images are expanded. // https://github.com/processing/processing/issues/1501 static private BufferedImage shrinkImage(BufferedImage img, int targetWidth, int targetHeight, int interpolationMode) { int type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; BufferedImage outgoing = img; BufferedImage scratchImage = null; Graphics2D g2 = null; int prevW = outgoing.getWidth(); int prevH = outgoing.getHeight(); boolean isTranslucent = img.getTransparency() != Transparency.OPAQUE; // Use multi-step technique: start with original size, then scale down in // multiple passes with drawImage() until the target size is reached int w = img.getWidth(); int h = img.getHeight(); do { if (w > targetWidth) { w /= 2; // if this is the last step, do the exact size if (w < targetWidth) { w = targetWidth; } } else { //if (targetWidth >= w) { w = targetWidth; } if (h > targetHeight) { h /= 2; if (h < targetHeight) { h = targetHeight; } } else { //if (targetHeight >= h) { h = targetHeight; } if (scratchImage == null || isTranslucent) { // Use a single scratch buffer for all iterations and then copy // to the final, correctly-sized image before returning scratchImage = new BufferedImage(w, h, type); g2 = scratchImage.createGraphics(); } // convert the passed int value of interpolationMode to the object expected // by setRenderingHint Object interpolationModeValue = switch(interpolationMode) { case 0 -> RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; //case 1 is the same as the default case 2 -> RenderingHints.VALUE_INTERPOLATION_BICUBIC; default -> RenderingHints.VALUE_INTERPOLATION_BILINEAR; }; g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationModeValue); g2.drawImage(outgoing, 0, 0, w, h, 0, 0, prevW, prevH, null); prevW = w; prevH = h; outgoing = scratchImage; } while (w != targetWidth || h != targetHeight); //if (g2 != null) { g2.dispose(); //} // If we used a scratch buffer that is larger than our target size, // create an image of the right size and copy the results into it if (targetWidth != outgoing.getWidth() || targetHeight != outgoing.getHeight()) { scratchImage = new BufferedImage(targetWidth, targetHeight, type); g2 = scratchImage.createGraphics(); g2.drawImage(outgoing, 0, 0, null); g2.dispose(); outgoing = scratchImage; } return outgoing; } static protected String[] loadImageExtensions; // list of ImageIO formats static public PImage loadImage(PApplet sketch, String filename, Object... args) { String extension = null; if (args != null && args.length > 0) { // the only one that's supported for now extension = (String) args[0]; } if (extension == null) { String lower = filename.toLowerCase(); int dot = filename.lastIndexOf('.'); if (dot == -1) { extension = "unknown"; // no extension found } else { extension = lower.substring(dot + 1); // check for, and strip any parameters on the url, i.e. // filename.jpg?blah=blah&something=that int question = extension.indexOf('?'); if (question != -1) { extension = extension.substring(0, question); } } } // just in case. them users will try anything! extension = extension.toLowerCase(); if (extension.equals("tga")) { try { InputStream input = sketch.createInput(filename); if (input == null) return null; PImage image = PImage.loadTGA(input); if (image != null) { image.parent = sketch; } return image; } catch (IOException e) { e.printStackTrace(); return null; } } // Disabling for 4.0 beta 5, we're now using ImageIO for TIFF /* if (extension.equals("tif") || extension.equals("tiff")) { InputStream input = sketch.createInput(filename); PImage image = (input == null) ? null : PImage.loadTIFF(input); return image; } */ // For jpeg, gif, and png, load them using createImage(), // because the javax.imageio code was found to be much slower. // https://download.processing.org/bugzilla/392.html try { if (extension.equals("jpg") || extension.equals("jpeg") || extension.equals("gif") || extension.equals("png") || extension.equals("unknown")) { byte[] bytes = sketch.loadBytes(filename); if (bytes == null) { return null; } else { //Image awtImage = Toolkit.getDefaultToolkit().createImage(bytes); Image awtImage = new ImageIcon(bytes).getImage(); if (awtImage instanceof BufferedImage) { BufferedImage buffImage = (BufferedImage) awtImage; int space = buffImage.getColorModel().getColorSpace().getType(); if (space == ColorSpace.TYPE_CMYK) { System.err.println(filename + " is a CMYK image, " + "only RGB images are supported."); return null; /* // wishful thinking, appears to not be supported // https://community.oracle.com/thread/1272045?start=0&tstart=0 BufferedImage destImage = new BufferedImage(buffImage.getWidth(), buffImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR); ColorConvertOp op = new ColorConvertOp(null); op.filter(buffImage, destImage); image = new PImage(destImage); */ } } PImage image = new PImageAWT(awtImage); if (image.width == -1) { System.err.println("The file " + filename + " contains bad image data, or may not be an image."); } // if it's a .gif image, test to see if it has transparency if (extension.equals("gif") || extension.equals("png") || extension.equals("unknown")) { image.checkAlpha(); } image.parent = sketch; return image; } } } catch (Exception e) { // show error, but move on to the stuff below, see if it'll work e.printStackTrace(); } if (loadImageExtensions == null) { loadImageExtensions = ImageIO.getReaderFormatNames(); } if (loadImageExtensions != null) { for (String loadImageExtension : loadImageExtensions) { if (extension.equals(loadImageExtension)) { return loadImageIO(sketch, filename); } } // failed, could not load image after all those attempts System.err.println("Could not load " + filename + ", " + "make sure it ends with a supported extension " + "(" + PApplet.join(loadImageExtensions, ", ") + ")"); } else { System.err.println("Could not load " + filename); } return null; } /** * Use Java 1.4 ImageIO methods to load an image. */ static protected PImage loadImageIO(PApplet sketch, String filename) { InputStream stream = sketch.createInput(filename); if (stream == null) { System.err.println("The image " + filename + " could not be found."); return null; } try { BufferedImage bi = ImageIO.read(stream); //PImage outgoing = new PImage(bi.getWidth(), bi.getHeight()); PImage outgoing = new PImageAWT(bi); outgoing.parent = sketch; //bi.getRGB(0, 0, outgoing.width, outgoing.height, // outgoing.pixels, 0, outgoing.width); // check the alpha for this image // was gonna call getType() on the image to see if RGB or ARGB, // but it's not actually useful, since gif images will come through // as TYPE_BYTE_INDEXED, which means it'll still have to check for // the transparency. also, would have to iterate through all the other // types and guess whether alpha was in there, so.. just gonna stick // with the old method. outgoing.checkAlpha(); stream.close(); // return the image return outgoing; } catch (Exception e) { e.printStackTrace(); return null; } } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . static public boolean saveImage(PImage image, String path, String... args) { if (saveImageExtensions == null) { saveImageExtensions = javax.imageio.ImageIO.getWriterFormatNames(); } try { if (saveImageExtensions != null) { for (String saveImageFormat : saveImageExtensions) { if (path.endsWith("." + saveImageFormat)) { if (!saveImageIO(image, path, args)) { System.err.println("Error while saving image."); return false; } return true; } } System.err.println("Could not save " + path + ", " + "make sure it ends with a supported extension " + "(" + PApplet.join(saveImageExtensions, ", ") + ")"); } } catch (IOException e) { e.printStackTrace(); } return false; } static protected String[] saveImageExtensions; /** * Use ImageIO functions from Java 1.4 and later to handle image save. * Various formats are supported, typically jpeg, png, bmp, and wbmp. * To get a list of the supported formats for writing, use:
* println(javax.imageio.ImageIO.getReaderFormatNames()) */ static protected boolean saveImageIO(PImage image, String path, String... args) throws IOException { try { int outputFormat = (image.format == ARGB) ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB; Map params = new HashMap<>(); params.put("quality", 0.9f); // default JPEG quality params.put("dpi", 100.0); // default DPI for PNG if (args != null) { for (String arg : args) { if (arg.startsWith("quality=")) { params.put("quality", Float.parseFloat(arg.substring(8))); } else if (arg.startsWith("dpi=")) { params.put("dpi", Double.parseDouble(arg.substring(4))); } } } String extension = path.substring(path.lastIndexOf('.') + 1).toLowerCase(); // JPEG and BMP images that have an alpha channel set get pretty unhappy. // BMP just doesn't write, and JPEG writes it as a CMYK image. // https://github.com/processing/processing/issues/454 if (extension.equals("bmp") || extension.equals("jpg") || extension.equals("jpeg")) { outputFormat = BufferedImage.TYPE_INT_RGB; } BufferedImage bimage = new BufferedImage(image.pixelWidth, image.pixelHeight, outputFormat); bimage.setRGB(0, 0, image.pixelWidth, image.pixelHeight, image.pixels, 0, image.pixelWidth); File file = new File(path); ImageWriter writer = null; ImageWriteParam param = null; IIOMetadata metadata = null; if (extension.equals("jpg") || extension.equals("jpeg")) { if ((writer = imageioWriter("jpeg")) != null) { // Set JPEG quality to 90% with baseline optimization. Setting this // to 1 was a huge jump (about triple the size), so this seems good. // Oddly, a smaller file size than Photoshop at 90%, but I suppose // it's a completely different algorithm. param = writer.getDefaultWriteParam(); param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); //param.setCompressionQuality(0.9f); param.setCompressionQuality((Float) params.get("quality")); } } if (extension.equals("png")) { if ((writer = imageioWriter("png")) != null) { param = writer.getDefaultWriteParam(); metadata = imageioDPI(writer, param, (Double) params.get("dpi")); } } if (writer != null) { OutputStream output = PApplet.createOutput(file); if (output == null) { return false; } writer.setOutput(ImageIO.createImageOutputStream(output)); writer.write(metadata, new IIOImage(bimage, null, metadata), param); writer.dispose(); output.flush(); output.close(); return true; } // If iter.hasNext() somehow fails up top, it falls through to here return javax.imageio.ImageIO.write(bimage, extension, file); } catch (Exception e) { e.printStackTrace(); throw new IOException("image save failed."); } } static private ImageWriter imageioWriter(String extension) { Iterator iter = ImageIO.getImageWritersByFormatName(extension); if (iter.hasNext()) { return iter.next(); } return null; } @SuppressWarnings("SameParameterValue") static private IIOMetadata imageioDPI(ImageWriter writer, ImageWriteParam param, double dpi) { // http://stackoverflow.com/questions/321736/how-to-set-dpi-information-in-an-image ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, param); if (!metadata.isReadOnly() && metadata.isStandardMetadataFormatSupported()) { // for PNG, it's dots per millimeter double dotsPerMilli = dpi / 25.4; IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize"); horiz.setAttribute("value", Double.toString(dotsPerMilli)); IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize"); vert.setAttribute("value", Double.toString(dotsPerMilli)); IIOMetadataNode dim = new IIOMetadataNode("Dimension"); dim.appendChild(horiz); dim.appendChild(vert); IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0"); root.appendChild(dim); try { metadata.mergeTree("javax_imageio_1.0", root); return metadata; } catch (IIOInvalidTreeException e) { System.err.println("Could not set the DPI of the output image"); e.printStackTrace(); } } return null; } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . static public void initRun() { // Supposed to help with flicker, but no effect on OS X. // TODO IIRC this helped on Windows, but need to double check. System.setProperty("sun.awt.noerasebackground", "true"); // Remove 60fps limit on the JavaFX "pulse" timer System.setProperty("javafx.animation.fullspeed", "true"); // Catch any HeadlessException to provide more useful feedback try { // Call validate() while resize events are in progress Toolkit.getDefaultToolkit().setDynamicLayout(true); } catch (HeadlessException e) { System.err.println("Cannot run sketch without a display. Read this for possible solutions:"); System.err.println("https://github.com/processing/processing/wiki/Running-without-a-Display"); System.exit(1); } } /* public int displayDensity() { if (sketch.display != PConstants.SPAN && (sketch.fullScreen || sketch.present)) { return displayDensity(sketch.display); } // walk through all displays, use 2 if any display is 2 for (int i = 0; i < displayDevices.length; i++) { if (displayDensity(i+1) == 2) { return 2; } } // If nobody's density is 2 then everyone is 1 return 1; } */ /** * display - the display number to check * (1-indexed to match the Preferences dialog box) */ /* public int displayDensity(int display) { if (display > 0 && display <= displayDevices.length) { GraphicsConfiguration graphicsConfig = displayDevices[display - 1].getDefaultConfiguration(); AffineTransform tx = graphicsConfig.getDefaultTransform(); return (int) Math.round(tx.getScaleX()); } System.err.println("Display " + display + " does not exist, returning "); return 1; // not the end of the world, so don't throw a RuntimeException } */ static public void selectInput(String prompt, String callbackMethod, File file, Object callbackObject) { EventQueue.invokeLater(() -> selectImpl(prompt, callbackMethod, file, callbackObject, null, FileDialog.LOAD)); } /* static public void selectOutput(String prompt, String callbackMethod, File file, Object callbackObject, Frame parent) { selectImpl(prompt, callbackMethod, file, callbackObject, parent, FileDialog.SAVE, null); } static public void selectOutput(String prompt, String callbackMethod, File file, Object callbackObject, Frame parent, PApplet sketch) { selectImpl(prompt, callbackMethod, file, callbackObject, parent, FileDialog.SAVE, sketch); } */ static public void selectOutput(String prompt, String callbackMethod, File file, Object callbackObject) { EventQueue.invokeLater(() -> selectImpl(prompt, callbackMethod, file, callbackObject, null, FileDialog.SAVE)); } /* // Will remove the 'sketch' parameter once we get an upstream JOGL fix // https://github.com/processing/processing/issues/3831 static protected void selectEvent(final String prompt, final String callbackMethod, final File defaultSelection, final Object callbackObject, final Frame parentFrame, final int mode, final PApplet sketch) { EventQueue.invokeLater(new Runnable() { public void run() { boolean hide = (sketch != null) && (sketch.g instanceof PGraphicsOpenGL) && (PApplet.platform == PConstants.WINDOWS); if (hide) sketch.getSurface().setVisible(false); selectImpl(prompt, callbackMethod, defaultSelection, callbackObject, parentFrame, mode, sketch); if (hide) sketch.getSurface().setVisible(true); } }); } */ static public void selectImpl(final String prompt, final String callbackMethod, final File defaultSelection, final Object callbackObject, final Frame parentFrame, final int mode) { selectImpl(prompt, defaultSelection, parentFrame, mode, (selectedFile) -> PApplet.selectCallback(selectedFile, callbackMethod, callbackObject) ); } static public void selectImpl(final String prompt, final File defaultSelection, final Frame parentFrame, final int mode, final Consumer callback) { File selectedFile = null; if (PApplet.useNativeSelect) { FileDialog dialog = new FileDialog(parentFrame, prompt, mode); if (defaultSelection != null) { dialog.setDirectory(defaultSelection.getParent()); dialog.setFile(defaultSelection.getName()); } dialog.setVisible(true); String directory = dialog.getDirectory(); String filename = dialog.getFile(); if (filename != null) { selectedFile = new File(directory, filename); } } else { JFileChooser chooser = new JFileChooser(); chooser.setDialogTitle(prompt); if (defaultSelection != null) { chooser.setSelectedFile(defaultSelection); } int result = -1; if (mode == FileDialog.SAVE) { result = chooser.showSaveDialog(parentFrame); } else if (mode == FileDialog.LOAD) { result = chooser.showOpenDialog(parentFrame); } if (result == JFileChooser.APPROVE_OPTION) { selectedFile = chooser.getSelectedFile(); } } callback.accept(selectedFile); } static public void selectFolder(final String prompt, final String callbackMethod, final File defaultSelection, final Object callbackObject) { EventQueue.invokeLater(() -> selectFolderImpl(prompt, callbackMethod, defaultSelection, callbackObject, null)); } static public void selectFolder(final String prompt, final File defaultSelection, final Consumer callback) { selectFolderImpl(prompt, defaultSelection, null, callback); } /* static public void selectFolder(final String prompt, final String callbackMethod, final File defaultSelection, final Object callbackObject, final Frame parentFrame) { selectFolderEvent(prompt, callbackMethod, defaultSelection, callbackObject, parentFrame, null); } // Will remove the 'sketch' parameter once we get an upstream JOGL fix // https://github.com/processing/processing/issues/3831 static public void selectFolderEvent(final String prompt, final String callbackMethod, final File defaultSelection, final Object callbackObject, final Frame parentFrame, final PApplet sketch) { EventQueue.invokeLater(() -> { selectFolderImpl(prompt, callbackMethod, defaultSelection, callbackObject, parentFrame, sketch); }); } */ static public void selectFolderImpl(final String prompt, final String callbackMethod, final File defaultSelection, final Object callbackObject, final Frame parentFrame) { selectFolderImpl(prompt, defaultSelection, parentFrame, (selectedFile) -> PApplet.selectCallback(selectedFile, callbackMethod, callbackObject) ); } static public void selectFolderImpl(final String prompt, final File defaultSelection, final Frame parentFrame, final Consumer callback) { File selectedFile = null; if (PApplet.platform == PConstants.MACOS && PApplet.useNativeSelect) { FileDialog fileDialog = new FileDialog(parentFrame, prompt, FileDialog.LOAD); if (defaultSelection != null) { fileDialog.setDirectory(defaultSelection.getAbsolutePath()); } System.setProperty("apple.awt.fileDialogForDirectories", "true"); fileDialog.setVisible(true); System.setProperty("apple.awt.fileDialogForDirectories", "false"); String filename = fileDialog.getFile(); if (filename != null) { selectedFile = new File(fileDialog.getDirectory(), fileDialog.getFile()); } } else { checkLookAndFeel(); JFileChooser fileChooser = new JFileChooser(); fileChooser.setDialogTitle(prompt); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (defaultSelection != null) { fileChooser.setCurrentDirectory(defaultSelection); } int result = fileChooser.showOpenDialog(parentFrame); if (result == JFileChooser.APPROVE_OPTION) { selectedFile = fileChooser.getSelectedFile(); } } callback.accept(selectedFile); } static private boolean lookAndFeelCheck; /** * Initialize the Look & Feel if it hasn't been already. * Call this before using any Swing-related code in PApplet methods. */ static private void checkLookAndFeel() { if (!lookAndFeelCheck) { if (PApplet.platform == PConstants.WINDOWS) { // Windows is defaulting to Metal or something else awful. // Which also is not scaled properly with HiDPI interfaces. try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception ignored) { } } lookAndFeelCheck = true; } } // TODO maybe call this with reflection from inside PApplet? // longer term, develop a more general method for other platforms static public File getWindowsDesktop() { return FileSystemView.getFileSystemView().getHomeDirectory(); } static public boolean openLink(String url) { try { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); if (desktop.isSupported(Desktop.Action.BROWSE)) { desktop.browse(new URI(url)); return true; } } } catch (IOException | URISyntaxException e) { e.printStackTrace(); } return false; } }