From f19d1f9c0f1f6ff5d535a0c6c601e668a038f974 Mon Sep 17 00:00:00 2001 From: stepan <stepan.sindelar@oracle.com> Date: Mon, 2 Oct 2017 20:30:54 +0200 Subject: [PATCH] Do AWT rendering properly in paint method --- .../r/library/fastrGrid/WindowDevice.java | 5 +- .../device/awt/BufferedJFrameDevice.java | 207 ------------ .../fastrGrid/device/awt/JFrameDevice.java | 315 +++++++++++++----- 3 files changed, 226 insertions(+), 301 deletions(-) delete mode 100644 com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedJFrameDevice.java diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java index 6ed69613fc..e805452d34 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java @@ -23,7 +23,6 @@ package com.oracle.truffle.r.library.fastrGrid; import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; -import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedJFrameDevice; import com.oracle.truffle.r.library.fastrGrid.device.awt.JFrameDevice; import com.oracle.truffle.r.runtime.RError; import com.oracle.truffle.r.runtime.RError.Message; @@ -42,13 +41,13 @@ public final class WindowDevice { } public static GridDevice createWindowDevice(int width, int height) { - JFrameDevice frameDevice = JFrameDevice.create(width, height); + JFrameDevice frameDevice = new JFrameDevice(width, height); if (RContext.getInstance().hasExecutor()) { frameDevice.setResizeListener(WindowDevice::redrawAll); } else { noSchedulingSupportWarning(); } - return new BufferedJFrameDevice(frameDevice); + return frameDevice; } public static RError awtNotSupported() { diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedJFrameDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedJFrameDevice.java deleted file mode 100644 index 427ec63104..0000000000 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedJFrameDevice.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package com.oracle.truffle.r.library.fastrGrid.device.awt; - -import static java.awt.image.BufferedImage.TYPE_INT_RGB; - -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.image.BufferStrategy; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; - -import javax.imageio.ImageIO; - -import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext; -import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; -import com.oracle.truffle.r.library.fastrGrid.device.ImageSaver; - -/** - * Decorator for {@link JFrameDevice} that implements {@link #hold()} and {@link #flush()} - * functionality and implements the {@link ImageSaver} device. - * - * Methods {@link #hold()} and {@link #flush()} open/draw a 2D graphics buffer, while the buffer is - * open, any drawing is done in the buffer not on the screen and the buffer is dumped to the screen - * once {@link #flush()} is called. - * - * We also record any drawing code to be able to replay it if the buffer happens to loose its - * contents, which is a possibility mentioned in the documentation. Moreover, this record of drawing - * can be used in {@link #save(String, String)} to replay the drawing in a {@code BufferedImage}. - * Note: here we rely on the fact that {@link DrawingContext} is immutable. - */ -public final class BufferedJFrameDevice implements GridDevice, ImageSaver { - private final JFrameDevice inner; - private final ArrayList<Runnable> drawActions = new ArrayList<>(200); - private BufferStrategy buffer; - - public BufferedJFrameDevice(JFrameDevice inner) { - this.inner = inner; - this.inner.setCloseListener(this::clearActions); - } - - @Override - public void openNewPage() { - inner.openNewPage(); - clearActions(); - } - - @Override - public void hold() { - if (buffer != null) { - return; // already buffering - } - buffer = inner.getCurrentFrame().getBufferStrategy(); - if (buffer == null) { - inner.getCurrentFrame().createBufferStrategy(2); - buffer = inner.getCurrentFrame().getBufferStrategy(); - } - drawActions.clear(); - setGraphics(buffer.getDrawGraphics()); - } - - @Override - public void flush() { - if (buffer == null) { - return; - } - - buffer.show(); - // re-draw the buffer if the contents were lost - while (buffer.contentsLost()) { - setGraphics(buffer.getDrawGraphics()); - for (Runnable drawAction : drawActions) { - drawAction.run(); - } - buffer.show(); - } - - setGraphics(inner.getCurrentFrame().getGraphics()); - buffer.dispose(); - buffer = null; - } - - @Override - public void close() throws DeviceCloseException { - inner.close(); - } - - @Override - public void drawRect(DrawingContext ctx, double leftX, double bottomY, double width, double height, double rotationAnticlockWise) { - inner.drawRect(ctx, leftX, bottomY, width, height, rotationAnticlockWise); - drawActions.add(() -> inner.drawRect(ctx, leftX, bottomY, width, height, rotationAnticlockWise)); - } - - @Override - public void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { - inner.drawPolyLines(ctx, x, y, startIndex, length); - double[] xCopy = new double[x.length]; - double[] yCopy = new double[y.length]; - System.arraycopy(x, 0, xCopy, 0, x.length); - System.arraycopy(y, 0, yCopy, 0, y.length); - drawActions.add(() -> inner.drawPolyLines(ctx, xCopy, yCopy, startIndex, length)); - } - - @Override - public void drawPolygon(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { - inner.drawPolygon(ctx, x, y, startIndex, length); - double[] xCopy = new double[x.length]; - double[] yCopy = new double[y.length]; - System.arraycopy(x, 0, xCopy, 0, x.length); - System.arraycopy(y, 0, yCopy, 0, y.length); - drawActions.add(() -> inner.drawPolygon(ctx, xCopy, yCopy, startIndex, length)); - } - - @Override - public void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) { - inner.drawCircle(ctx, centerX, centerY, radius); - drawActions.add(() -> inner.drawCircle(ctx, centerX, centerY, radius)); - } - - @Override - public void drawRaster(double leftX, double bottomY, double width, double height, int[] pixels, int pixelsColumnsCount, ImageInterpolation interpolation) { - inner.drawRaster(leftX, bottomY, width, height, pixels, pixelsColumnsCount, interpolation); - drawActions.add(() -> inner.drawRaster(leftX, bottomY, width, height, pixels, pixelsColumnsCount, interpolation)); - } - - @Override - public void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) { - inner.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text); - drawActions.add(() -> inner.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text)); - } - - @Override - public double getWidth() { - return inner.getWidth(); - } - - @Override - public double getHeight() { - return inner.getHeight(); - } - - @Override - public double getStringWidth(DrawingContext ctx, String text) { - return inner.getStringWidth(ctx, text); - } - - @Override - public double getStringHeight(DrawingContext ctx, String text) { - return inner.getStringHeight(ctx, text); - } - - @Override - public void save(String path, String fileType) throws IOException { - int realWidth = (int) (getWidth() * JFrameDevice.AWT_POINTS_IN_INCH); - int readHeight = (int) (getHeight() * JFrameDevice.AWT_POINTS_IN_INCH); - BufferedImage image = new BufferedImage(realWidth, readHeight, TYPE_INT_RGB); - Graphics2D imageGraphics = (Graphics2D) image.getGraphics(); - imageGraphics.setBackground(new Color(255, 255, 255)); - imageGraphics.clearRect(0, 0, realWidth, readHeight); - setGraphics(imageGraphics); - for (Runnable drawAction : drawActions) { - drawAction.run(); - } - ImageIO.write(image, fileType, new File(path)); - setGraphics(inner.getCurrentFrame().getGraphics()); - } - - private void clearActions() { - drawActions.clear(); - if (buffer != null) { - // if new page is opened while we are on hold, we should throw away current buffer. In - // other words that is like starting new hold without previous flush. - buffer.dispose(); - buffer = null; - hold(); - } - } - - private void setGraphics(Graphics graphics) { - Graphics2DDevice.defaultInitGraphics((Graphics2D) graphics); - inner.getGraphics2D().dispose(); - inner.setGraphics2D((Graphics2D) graphics); - } -} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/JFrameDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/JFrameDevice.java index a284e5e34c..e607c2a273 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/JFrameDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/JFrameDevice.java @@ -22,73 +22,156 @@ */ package com.oracle.truffle.r.library.fastrGrid.device.awt; -import java.awt.BorderLayout; +import static com.oracle.truffle.r.library.fastrGrid.device.awt.Graphics2DDevice.defaultInitGraphics; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; + import java.awt.Dimension; +import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.HeadlessException; -import java.awt.Toolkit; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; import java.util.Timer; import java.util.TimerTask; +import javax.imageio.ImageIO; import javax.swing.JFrame; import javax.swing.JPanel; -public final class JFrameDevice extends Graphics2DDevice { +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; +import com.oracle.truffle.r.library.fastrGrid.device.ImageSaver; + +/** + * This device paints everything into an image, which is painted into a AWT component in its + * {@code paint} method. + */ +public final class JFrameDevice implements GridDevice, ImageSaver { + + // This will be drawn on the component + private BufferedImage componentImage; + // Grid draings will be made into this image, may be == componentImage if isOnHold is false + private BufferedImage image; + // If have we created a new image for buffering while on hold, we keep it to reuse it + private BufferedImage cachedImage; - private final FastRFrame currentFrame; + private Graphics2D graphics; + private Graphics2DDevice inner; + private boolean isOnHold = false; + + private FastRFrame currentFrame; private Runnable onResize; private Runnable onClose; - /** - * @param frame The frame that should be used for drawing. - * @param graphics The graphics object that should be used for drawing. This constructor - * overload allows to initialize the graphics object. . - */ - private JFrameDevice(FastRFrame frame, Graphics2D graphics) { - super(graphics, frame.getContentPane().getWidth(), frame.getContentPane().getHeight(), true); - currentFrame = frame; - currentFrame.device = this; + public JFrameDevice(int width, int height) { + currentFrame = new FastRFrame(new FastRPanel(width, height)); + openGraphics2DDevice(currentFrame.fastRComponent.getWidth(), currentFrame.fastRComponent.getHeight()); + componentImage = image; } - /** - * Creates a standalone device that manages the window itself and closes it once - * {@link #close()} gets called. - */ - public static JFrameDevice create(int width, int height) { - FastRFrame frame = new FastRFrame(width, height); - Graphics2D graphics = initFrame(frame); - graphics.clearRect(0, 0, width, height); - return new JFrameDevice(frame, graphics); + @Override + public synchronized void openNewPage() { + inner.openNewPage(); + repaint(); + } + + @Override + public void hold() { + isOnHold = true; + if (cachedImage == null) { + cachedImage = new BufferedImage(inner.getWidthAwt(), inner.getHeightAwt(), TYPE_INT_RGB); + } + // we keep the original image so that we have the original screen state to paint while on + // hold and drawing new stuff into the "image" buffer + componentImage = image; + image = cachedImage; + graphics.dispose(); + graphics = (Graphics2D) image.getGraphics(); + defaultInitGraphics(graphics); + // new drawings should be built on top of the old screen, not whatever was in the buffer + graphics.drawImage(componentImage, 0, 0, null); + inner.setGraphics2D(graphics); + } + + @Override + public void flush() { + isOnHold = false; + cachedImage = componentImage; + componentImage = image; + repaint(); } @Override public void close() throws DeviceCloseException { - getGraphics2D().dispose(); + disposeImageDevice(); currentFrame.dispose(); + componentImage = null; } @Override - int getWidthAwt() { - return currentFrame.getContentPane().getWidth(); + public synchronized void drawRect(DrawingContext ctx, double leftX, double bottomY, double width, double height, double rotationAnticlockWise) { + inner.drawRect(ctx, leftX, bottomY, width, height, rotationAnticlockWise); + repaint(); } @Override - int getHeightAwt() { - return currentFrame.getContentPane().getHeight(); + public synchronized void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + inner.drawPolyLines(ctx, x, y, startIndex, length); + repaint(); } @Override - void ensureOpen() { - if (!currentFrame.isVisible()) { - getGraphics2D().dispose(); - setGraphics2D(initFrame(currentFrame)); - // TODO: for some reason this does not always clear the whole page. - getGraphics2D().clearRect(0, 0, currentFrame.getWidth(), currentFrame.getHeight()); - } + public synchronized void drawPolygon(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + inner.drawPolygon(ctx, x, y, startIndex, length); + repaint(); + } + + @Override + public synchronized void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) { + inner.drawCircle(ctx, centerX, centerY, radius); + repaint(); + } + + @Override + public synchronized void drawRaster(double leftX, double bottomY, double width, double height, int[] pixels, int pixelsColumnsCount, ImageInterpolation interpolation) { + inner.drawRaster(leftX, bottomY, width, height, pixels, pixelsColumnsCount, interpolation); + repaint(); + } + + @Override + public synchronized void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) { + inner.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text); + repaint(); + } + + @Override + public double getWidth() { + return inner.getWidth(); + } + + @Override + public double getHeight() { + return inner.getHeight(); + } + + @Override + public double getStringWidth(DrawingContext ctx, String text) { + return inner.getStringWidth(ctx, text); + } + + @Override + public double getStringHeight(DrawingContext ctx, String text) { + return inner.getStringHeight(ctx, text); + } + + @Override + public void save(String path, String fileType) throws IOException { + ImageIO.write(image, fileType, new File(path)); } public void setResizeListener(Runnable onResize) { @@ -99,91 +182,141 @@ public final class JFrameDevice extends Graphics2DDevice { this.onClose = onClose; } - JFrame getCurrentFrame() { - return currentFrame; + private synchronized void openGraphics2DDevice(int width, int height) { + image = new BufferedImage(width, height, TYPE_INT_RGB); + graphics = (Graphics2D) image.getGraphics(); + defaultInitGraphics(graphics); + graphics.clearRect(0, 0, width, height); + inner = new Graphics2DDevice(graphics, width, height, true); } - private static Graphics2D initFrame(FastRFrame frame) { - frame.setVisible(true); - frame.pack(); - Graphics2D graphics = (Graphics2D) frame.getGraphics(); - defaultInitGraphics(graphics); - return graphics; + private void disposeImageDevice() { + try { + inner.close(); + } catch (DeviceCloseException e) { + throw new RuntimeException(e); + } + if (graphics != null) { + graphics.dispose(); + } + image = null; + cachedImage = null; } - static class FastRFrame extends JFrame { - private static final long serialVersionUID = 1L; - private final JPanel fastRComponent = new JPanel(); - private JFrameDevice device; + private void resize(int newWidth, int newHeight) { + disposeImageDevice(); + openGraphics2DDevice(newWidth, newHeight); + if (onResize != null) { + onResize.run(); + } + } + private void ensureOpen() { + if (!currentFrame.isVisible()) { + int width = inner.getWidthAwt(); + int height = inner.getHeightAwt(); + currentFrame = new FastRFrame(new FastRPanel(width, height)); + disposeImageDevice(); + openGraphics2DDevice(width, height); + } + } + + private void repaint() { + if (!isOnHold) { + ensureOpen(); + currentFrame.repaint(); + } + } + + /** + * The component where the drawing will be shown. + */ + class FastRPanel extends JPanel { + private static final long serialVersionUID = 1234321L; + + // To avoid resizing too quickly while the user is still changing the size, we throttle the + // resizing using this timer volatile boolean resizeScheduled = false; + private final Timer timer = new Timer(); + + // To avoid running the resizing code if the size actually did not change private int oldWidth; private int oldHeight; - private final Timer timer = new Timer(); - FastRFrame(int width, int height) throws HeadlessException { - super("FastR"); - addListeners(); - createUI(width, height); - center(); + FastRPanel(int width, int height) { + setPreferredSize(new Dimension(width, height)); oldWidth = width; oldHeight = height; } - private void createUI(int width, int height) { - setLayout(new BorderLayout()); - setSize(new Dimension(width, height)); - add(fastRComponent, BorderLayout.CENTER); - fastRComponent.setPreferredSize(getSize()); + @Override + public void paint(Graphics g) { + BufferedImage toDraw = JFrameDevice.this.componentImage; + if (toDraw == null) { + super.paint(g); + return; + } + synchronized (JFrameDevice.this) { + Graphics2D graphics = (Graphics2D) g; + graphics.drawImage(toDraw, 0, 0, null); + } } - private void addListeners() { - addWindowFocusListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - if (device.onClose != null) { - device.onClose.run(); - } - } - }); - addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - if (oldHeight == getHeight() && oldWidth == getWidth()) { - return; - } - if (!resizeScheduled) { - resizeScheduled = true; - scheduleResize(); - } - } - }); + void resized() { + if (oldHeight == getHeight() && oldWidth == getWidth()) { + return; + } + if (!resizeScheduled) { + resizeScheduled = true; + scheduleResize(); + } } private void scheduleResize() { timer.schedule(new TimerTask() { @Override public void run() { - if (device == null) { - scheduleResize(); - return; - } oldWidth = getWidth(); oldHeight = getHeight(); resizeScheduled = false; - if (device.onResize != null) { - device.onResize.run(); - } + JFrameDevice.this.resize(oldWidth, oldHeight); } }, 1000); } + } + + /** + * The window that wraps the {@link FastRPanel}. + */ + class FastRFrame extends JFrame { + private static final long serialVersionUID = 187211L; + private final FastRPanel fastRComponent; + + FastRFrame(FastRPanel fastRComponent) throws HeadlessException { + super("FastR"); + this.fastRComponent = fastRComponent; + addListeners(); + getContentPane().add(fastRComponent); + pack(); + setLocationRelativeTo(null); // centers the window + setVisible(true); + } - private void center() { - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - Dimension frameSize = getSize(); - int x = (screenSize.width - frameSize.width) / 2; - int y = (screenSize.height - frameSize.height) / 2; - setLocation(x, y); + private void addListeners() { + addWindowFocusListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + if (onClose != null) { + onClose.run(); + } + } + }); + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + fastRComponent.resized(); + } + }); } } } -- GitLab