Skip to content
Snippets Groups Projects
Commit f19d1f9c authored by stepan's avatar stepan
Browse files

Do AWT rendering properly in paint method

parent d8a6f262
No related branches found
No related tags found
No related merge requests found
......@@ -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() {
......
/*
* 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);
}
}
......@@ -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();
}
});
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment