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