!! Warning !!
This example makes a number of assumptions:
- We're only dealing with a
PlainDocument
(ie only supports JTextArea
)
- We're only dealing with "plain text" - the example doesn't do a lot in the way of checking the available data flavours from the clipboard and only supports
DataFlavor.stringFlavor
Example...
After spending some time digging around, the current "paste" operation is, complicated, and it would be a non-trivial amount of work to replicate it - because making it work on a background thread as is would be impossible.
So, instead, we need to step back and take a look at how we could take control of it.
My test data consisted of plain text of 1, 178, 241 lines/42, 094, 019 characters.
My first thought was to split text from the clipboard into smaller chunks and then simply insert those chunks into the document. This actually turned out to be way worse then I first expected.
My next thought was to make a copy of the underlying Document
and update this copy on a background thread and once completed, apply the copy to the text area itself.
This did work. But, interestingly, it raised a copy of points.
- Pulling the text from the clipboard is relatively fast (in may testing 0.529 milliseconds)
- Inserting the text into the document is relatively fast (in my testing 0.608s)
- Updating the view with the text is also slow (~1-2s)
Take into account that it's taken rough 1-2s before the view is even been updated and we're running into the 3-4s mark. Also add in the overhead of creating the SwingWorker
and it's own internal management of sync back to the Event Dispatching Thread (in that case you might need to do some more testing and decide at what threshold it's worth spinning up the SwingWorker
for)
Runnable example
!! Cavet: This just a demonstration of the core concepts, there is so much more that really needs to be taken into consideration, for example:
- Better error handling
- Investigation into the desired threshold when it would be justifiable to spin up a
SwingWorker
to do this work.
- Pre-sampling of the clipboard - do things like, look at the supported
DataFlavor
s to determine if it contains plain text, maybe even look at the size of the content, if it's fast enough to do so.

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutionException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new BorderLayout());
JTextArea ta = new JTextArea(20, 40);
Action action = ta.getActionMap().get("paste-from-clipboard");
ta.getActionMap().put("paste-from-clipboard", new BackgroundPasteAction());
add(new JScrollPane(ta));
}
}
public class BackgroundPasteAction extends AbstractAction {
public BackgroundPasteAction() {
}
@Override
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
if (!(source instanceof JTextArea)) {
return;
}
JTextArea textArea = (JTextArea) source;
JRootPane rootPane = SwingUtilities.getRootPane(textArea);
Component oldGlassPane = rootPane.getGlassPane();
WaitOverlay overlay = new WaitOverlay();
rootPane.setGlassPane(overlay);
overlay.start();
overlay.setVisible(true);
setEnabled(false);
PasteWorker worker = new PasteWorker(textArea);
StopWatch sw = new StopWatch().start();
worker.addPropertyChangeListener(new SwingWorkerAdapter<PasteWorker>() {
@Override
protected void workerDone(PasteWorker worker) {
try {
textArea.setDocument(worker.get());
textArea.setCaretPosition(worker.getNewCaretPosition());
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
setEnabled(true);
overlay.stop();
overlay.setVisible(false);
rootPane.setGlassPane(oldGlassPane);
System.out.println("Job took: " + sw.stop());
}
});
worker.execute();
}
}
// This makes it easier to deal with the PropertyChangeListener support of
// SwingWorker and allows you to focus on only the states you are interested
// in
public class SwingWorkerAdapter<W extends SwingWorker> implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
W worker = (W) evt.getSource();
switch (evt.getPropertyName()) {
case "state":
workerStateChanged(worker);
switch (worker.getState()) {
case STARTED:
workerStarted(worker);
break;
case DONE:
workerDone(worker);
break;
}
break;
case "progress":
workerProgressUpdated(worker);
break;
}
}
protected void workerProgressUpdated(W worker) {
}
protected void workerStateChanged(W worker) {
}
protected void workerStarted(W worker) {
}
protected void workerDone(W worker) {
}
}
// The SwingWorker which actually does the heavy lifting. It takes the text
// from the clipboard and inserts into a copy of the Document
public class PasteWorker extends SwingWorker<Document, Void> {
private JTextArea textArea;
private int initialCaretPosition;
private int newCaretPosition;
public PasteWorker(JTextArea textArea) {
this.textArea = textArea;
this.initialCaretPosition = textArea.getCaretPosition();
}
public int getInitialCaretPosition() {
return initialCaretPosition;
}
public JTextArea getTextArea() {
return textArea;
}
public int getNewCaretPosition() {
return newCaretPosition;
}
@Override
protected Document doInBackground() throws Exception {
Document document = new PlainDocument();
document.insertString(0, textArea.getText(), null);
StopWatch sw = new StopWatch().start();
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
String text = clipboard.getContents(null).getTransferData(DataFlavor.stringFlavor).toString();
Instant now = Instant.now();
System.out.println("Clipboard took: " + sw.stop());
sw.start();
document.insertString(getInitialCaretPosition(), text, null);
System.out.println("Paste took: " + sw.stop());
newCaretPosition = getInitialCaretPosition() + text.length();
return document;
}
}
// The actuall overlay view - used on the glass pane
public class WaitOverlay extends JPanel {
private WaitIndicator indicator = new WaitIndicator();
public WaitOverlay() {
setOpaque(false);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(0, 4, 0, 4);
add(indicator, gbc);
add(new JLabel("Pasting..."), gbc);
}
public void start() {
indicator.start();
}
public void stop() {
indicator.stop();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.SrcOver.derive(0.75f));
g2d.setColor(getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.dispose();
}
}
// An implementation of an animated "wait" indicator
public class WaitIndicator extends JPanel {
private double angle;
private double extent;
private double angleDelta = -1;
private double extentDelta = -5;
private boolean flip = false;
private Timer timer;
public WaitIndicator() {
setForeground(Color.BLACK);
timer = new Timer(10, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
angle += angleDelta;
extent += extentDelta;
if (Math.abs(extent) % 360.0 == 0) {
angle = angle - extent;
flip = !flip;
if (flip) {
extent = 360.0;
} else {
extent = 0.0;
}
}
repaint();
}
});
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(25, 25);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
int width = getWidth();
int height = getHeight();
int size = Math.min(width, height);
int x = (getWidth() - size) / 2;
int y = (getHeight() - size) / 2;
Arc2D.Double arc = new Arc2D.Double(x + 2, y + 2, width - 4, height - 4, angle, extent, Arc2D.OPEN);
BasicStroke stroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND);
g2d.setStroke(stroke);
g2d.setColor(getForeground());
g2d.draw(arc);
g2d.dispose();
}
}
// Utility classed used for calculating the time difference between two
// points in time - it's just simpler then having a lot of duplicate
// code laying around
public class StopWatch {
private LocalDateTime startTime;
private Duration totalRunTime = Duration.ZERO;
public StopWatch start() {
totalRunTime = Duration.ZERO;
startTime = LocalDateTime.now();
return this;
}
public StopWatch stop() {
if (startTime == null) {
return this;
}
totalRunTime = Duration.between(startTime, LocalDateTime.now());
startTime = null;
return this;
}
public void reset() {
stop();
totalRunTime = Duration.ZERO;
}
public boolean isRunning() {
return startTime != null;
}
public Duration getDuration() {
Duration currentDuration = Duration.ZERO;
currentDuration = currentDuration.plus(totalRunTime);
if (isRunning()) {
Duration runTime = Duration.between(startTime, LocalDateTime.now());
currentDuration = currentDuration.plus(runTime);
}
return currentDuration;
}
@Override
public String toString() {
Duration duration = getDuration();
return String.format("%02d:%03d", duration.toSecondsPart(), duration.toMillisPart());
}
}
}