Add Clipboard Support

Clipboard support (read and write) is only available in Chromium-based
browsers (Chrome, Opera, new Edge). It's only possible to transfer text
content. Current implementation requires SWT_COMPATIBILITY operational
mode as it's a synchronous operation.

Bug 360790: Add Clipboard Support to RAP
https://bugs.eclipse.org/bugs/show_bug.cgi?id=360790

Change-Id: I6b4e779c3da5b7086331e583c7c6c8afb070ef52
diff --git a/bundles/org.eclipse.rap.rwt/js/rwt/client/Clipboard.js b/bundles/org.eclipse.rap.rwt/js/rwt/client/Clipboard.js
new file mode 100644
index 0000000..1d4456e
--- /dev/null
+++ b/bundles/org.eclipse.rap.rwt/js/rwt/client/Clipboard.js
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * Copyright (c) 2020 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+
+namespace( "rwt.client" );
+
+(function() {
+
+  rwt.client.Clipboard = {
+
+    writeText: function( properties ) {
+      var remoteObject = rap.getRemoteObject( this );
+      if( navigator.clipboard && navigator.clipboard.writeText ) {
+        navigator.clipboard.writeText( properties.text ).then( function() {
+          remoteObject.call( "operationSucceeded", {
+            "operation" : "writeText",
+            "result" : ""
+          } );
+        }, function( error ) {
+          remoteObject.call( "operationFailed", {
+            "operation" : "writeText",
+            "errorMessage" : error.message
+          } );
+        } );
+      } else {
+        remoteObject.call( "operationFailed", {
+          "operation" : "writeText",
+          "errorMessage" : "Clipboard operation writeText is not supported."
+        } );
+      }
+    },
+
+    readText: function() {
+      var remoteObject = rap.getRemoteObject( this );
+      if( navigator.clipboard && navigator.clipboard.readText ) {
+        navigator.clipboard.readText().then( function( text ) {
+          remoteObject.call( "operationSucceeded", {
+            "operation" : "readText",
+            "result" : text
+          } );
+        }, function( error ) {
+          remoteObject.call( "operationFailed", {
+            "operation" : "readText",
+            "errorMessage" : error.message
+          } );
+        } );
+      } else {
+        remoteObject.call( "operationFailed", {
+          "operation" : "readText",
+          "errorMessage" : "Clipboard operation readText is not supported."
+        } );
+      }
+    }
+
+  };
+
+})();
diff --git a/bundles/org.eclipse.rap.rwt/js/rwt/remote/handler/ClipboardHandler.js b/bundles/org.eclipse.rap.rwt/js/rwt/remote/handler/ClipboardHandler.js
new file mode 100644
index 0000000..ad26b82
--- /dev/null
+++ b/bundles/org.eclipse.rap.rwt/js/rwt/remote/handler/ClipboardHandler.js
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * Copyright (c) 2020 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+
+rwt.remote.HandlerRegistry.add( "rwt.client.Clipboard", {
+
+  factory : function() {
+    return rwt.client.Clipboard;
+  },
+
+  methods : [ "writeText", "readText" ]
+
+} );
diff --git a/bundles/org.eclipse.rap.rwt/resources/client.files b/bundles/org.eclipse.rap.rwt/resources/client.files
index 8a67db4..2339c57 100644
--- a/bundles/org.eclipse.rap.rwt/resources/client.files
+++ b/bundles/org.eclipse.rap.rwt/resources/client.files
@@ -265,3 +265,5 @@
 rwt/widgets/DropDown.js
 rwt/widgets/util/DropDownSynchronizer.js
 rwt/remote/handler/DropDownHandler.js
+rwt/client/Clipboard.js
+rwt/remote/handler/ClipboardHandler.js
diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/dnd/Clipboard.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/dnd/Clipboard.java
new file mode 100644
index 0000000..6de317a
--- /dev/null
+++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/dnd/Clipboard.java
@@ -0,0 +1,450 @@
+/*******************************************************************************
+ * Copyright (c) 2020 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.swt.dnd;
+
+import static org.eclipse.rap.rwt.internal.service.ContextProvider.getApplicationContext;
+
+import org.eclipse.rap.json.JsonObject;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.internal.lifecycle.ProcessActionRunner;
+import org.eclipse.rap.rwt.internal.lifecycle.SimpleLifeCycle;
+import org.eclipse.rap.rwt.remote.AbstractOperationHandler;
+import org.eclipse.rap.rwt.remote.RemoteObject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTError;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * The <code>Clipboard</code> provides a mechanism for transferring data from one
+ * application to another or within an application.
+ *
+ * <p>IMPORTANT: This class is <em>not</em> intended to be subclassed.</p>
+ *
+ * @noextend This class is not intended to be subclassed by clients.
+ * @since 3.14
+ */
+public class Clipboard {
+
+  private static final String REMOTE_TYPE = "rwt.client.Clipboard";
+  private static final String PROP_TEXT = "text";
+  private static final String PROP_OPERATION = "operation";
+  private static final String PROP_RESULT = "result";
+  private static final String PROP_ERROR_MESSAGE = "errorMessage";
+  private static final String METHOD_OPERATION_SUCCEEDED = "operationSucceeded";
+  private static final String METHOD_OPERATION_FAILED = "operationFailed";
+  private static final String METHOD_READ_TEXT = "readText";
+  private static final String METHOD_WRITE_TEXT = "writeText";
+
+  enum ClipboardOperation {
+    WRITE_TEXT,
+    READ_TEXT
+  }
+
+  private Display display;
+  private final RemoteObject remoteObject;
+  private ClipboardListener listener;
+  private boolean operationPending;
+  private Object operationResult;
+
+  /**
+   * Constructs a new instance of this class.
+   *
+   * @param display the display on which to allocate the clipboard
+   *
+   * @exception SWTException <ul>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
+   *    <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
+   * </ul>
+   *
+   * @see Clipboard#checkSubclass
+   */
+  public Clipboard( Display display ) {
+    checkSubclass();
+    this.display = display;
+    if( this.display == null ) {
+      this.display = Display.getCurrent();
+      if( this.display == null ) {
+        this.display = Display.getDefault();
+      }
+    }
+    if( this.display.getThread() != Thread.currentThread() ) {
+      DND.error( SWT.ERROR_THREAD_INVALID_ACCESS );
+    }
+    remoteObject = RWT.getUISession().getConnection().createRemoteObject( REMOTE_TYPE );
+    remoteObject.setHandler( new ClipboardOperationHandler() );
+  }
+
+  /**
+   * Sets the listener who will be notified when clipboard operation is performed, by sending
+   * it one of the messages defined in the <code>ClipboardListener</code> interface.
+   *
+   * @param listener the listener which should be notified
+   *
+   * @exception IllegalArgumentException <ul>
+   *     <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
+   * </ul>
+   * @exception SWTException <ul>
+   *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
+   * </ul>
+   *
+   * @see ClipboardListener
+   */
+  void setClipboardListener( ClipboardListener listener ) {
+    checkWidget();
+    if( listener == null ) {
+      DND.error( SWT.ERROR_NULL_ARGUMENT );
+    }
+    this.listener = listener;
+  }
+
+  /**
+   * Place text data in the client clipboard.
+   *
+   * @param text the text to placed in the clipboard
+   *
+   * @exception IllegalArgumentException <ul>
+   *     <li>ERROR_NULL_ARGUMENT - if the text is null</li>
+   * </ul>
+   * @exception SWTException <ul>
+   *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
+   * </ul>
+   */
+  void writeText( String text ) {
+    checkWidget();
+    if( text == null ) {
+      DND.error( SWT.ERROR_NULL_ARGUMENT );
+    }
+    remoteObject.call( METHOD_WRITE_TEXT, new JsonObject().add( PROP_TEXT, text ) );
+  }
+
+  /**
+   * Ask the client to get the text data from the clipboard.
+   *
+   * To get the actual data you have to add ClipboardListener before calling this method.
+   *
+   * @exception SWTException <ul>
+   *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
+   * </ul>
+   *
+   * @see ClipboardListener
+   */
+  void readText() {
+    checkWidget();
+    remoteObject.call( METHOD_READ_TEXT, null );
+  }
+
+  /**
+   * Retrieve the data of the specified type currently available on the system
+   * clipboard. Refer to the specific subclass of <code>Transfer</code> to
+   * determine the type of object returned. Only <code>TextTransfer</code> is currently supported.
+   *
+   * <p>The following snippet shows text being retrieved from the
+   * clipboard:</p>
+   *
+   *    <code><pre>
+   *    Clipboard clipboard = new Clipboard(display);
+   *    TextTransfer textTransfer = TextTransfer.getInstance();
+   *    String textData = (String)clipboard.getContents(textTransfer);
+   *    if (textData != null) System.out.println("Text is "+textData);
+   *    </code></pre>
+   *
+   * @param transfer the transfer agent for the type of data being requested
+   * @return the data obtained from the clipboard or null if no data of this type is available
+   *
+   * @exception SWTException <ul>
+   *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
+   * </ul>
+   * @exception IllegalArgumentException <ul>
+   *    <li>ERROR_NULL_ARGUMENT - if transfer is null</li>
+   * </ul>
+   *
+   * @exception UnsupportedOperationException when running the application in JEE_COMPATIBILITY mode
+   * @exception IllegalStateException when clipboard content is already requested.
+   *
+   * @see org.eclipse.rap.rwt.application.Application.OperationMode
+   * @see Transfer
+   */
+  public Object getContents( Transfer transfer ) {
+    checkOperationMode();
+    checkWidget();
+    if( transfer == null ) {
+      DND.error( SWT.ERROR_NULL_ARGUMENT );
+    }
+    if( !( transfer instanceof TextTransfer ) ) {
+      DND.error( SWT.ERROR_INVALID_ARGUMENT );
+    }
+    if( operationPending ) {
+      throw new IllegalStateException( "Another clipboard operation is already pending" );
+    }
+    operationPending = true;
+    operationResult = null;
+    readText();
+    while( operationResult == null ) {
+      if( !display.readAndDispatch() ) {
+        display.sleep();
+      }
+    }
+    operationPending = false;
+    return operationResult instanceof SWTException ? null : operationResult;
+  }
+
+  /**
+   * Place data of the specified type on the system clipboard.  Only one type
+   * of data can be placed on the system clipboard at the same time.  Setting the
+   * data clears any previous data from the system clipboard, regardless of type.
+   * Only <code>TextTransfer</code> is currently supported.
+   *
+   * <p>The following snippet shows text being set on the copy/paste clipboard:
+   * </p>
+   *
+   * <code><pre>
+   *  Clipboard clipboard = new Clipboard(display);
+   *  String textData = "Hello World";
+   *  TextTransfer textTransfer = TextTransfer.getInstance();
+   *  Transfer[] transfers = new Transfer[]{textTransfer};
+   *  Object[] data = new Object[]{textData};
+   *  clipboard.setContents(data, transfers);
+   * </code></pre>
+   *
+   * @param data the data to be set in the clipboard
+   * @param dataTypes the transfer agents that will convert the data to its
+   * platform specific format; each entry in the data array must have a
+   * corresponding dataType
+   *
+   * @exception IllegalArgumentException <ul>
+   *    <li>ERROR_INVALID_ARGUMENT - if data is null or datatypes is null
+   *          or the length of data is not the same as the length of dataTypes</li>
+   * </ul>
+   * @exception SWTException <ul>
+   *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
+   * </ul>
+   *  @exception SWTError <ul>
+   *    <li>ERROR_CANNOT_SET_CLIPBOARD - if the clipboard is locked or otherwise unavailable</li>
+   * </ul>
+   *
+   * @exception UnsupportedOperationException when running the application in JEE_COMPATIBILITY mode
+   * @exception IllegalStateException when clipboard content is already requested.
+   *
+   * @see org.eclipse.rap.rwt.application.Application.OperationMode
+   */
+  public void setContents( Object[] data, Transfer[] dataTypes ) {
+    checkOperationMode();
+    checkWidget();
+    if( data == null || dataTypes == null || data.length != dataTypes.length || data.length != 1 ) {
+      DND.error( SWT.ERROR_INVALID_ARGUMENT );
+    }
+    for( int i = 0; i < data.length; i++ ) {
+      if(    data[ i ] == null
+          || dataTypes[ i ] == null
+          || !( dataTypes[ i ] instanceof TextTransfer )
+          || !dataTypes[ i ].validate( data[ i ] ) )
+      {
+        DND.error( SWT.ERROR_INVALID_ARGUMENT );
+      }
+    }
+    if( operationPending ) {
+      throw new IllegalStateException( "Another clipboard operation is already pending" );
+    }
+    operationPending = true;
+    operationResult = null;
+    writeText( ( String )data[ 0 ] );
+    while( operationResult == null ) {
+      if( !display.readAndDispatch() ) {
+        display.sleep();
+      }
+    }
+    operationPending = false;
+    if( operationResult instanceof SWTException ) {
+      DND.error( DND.ERROR_CANNOT_SET_CLIPBOARD );
+    }
+  }
+
+  /**
+   * Disposes of the operating system resources associated with the clipboard.
+   * The data will still be available on the system clipboard after the dispose
+   * method is called.
+   *
+   * @exception SWTException <ul>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
+   * </ul>
+   */
+  public void dispose() {
+    if( isDisposed() ) {
+      return;
+    }
+    if( display.getThread() != Thread.currentThread() ) {
+      DND.error( SWT.ERROR_THREAD_INVALID_ACCESS );
+    }
+    display = null;
+    remoteObject.destroy();
+  }
+
+  /**
+   * Returns <code>true</code> if the clipboard has been disposed,
+   * and <code>false</code> otherwise.
+   * <p>
+   * This method gets the dispose state for the clipboard.
+   * When a clipboard has been disposed, it is an error to
+   * invoke any other method using the clipboard.
+   * </p>
+   *
+   * @return <code>true</code> when the widget is disposed and <code>false</code> otherwise
+   */
+  public boolean isDisposed() {
+    return display == null;
+  }
+
+  /**
+   * Throws an <code>SWTException</code> if the receiver can not
+   * be accessed by the caller. This may include both checks on
+   * the state of the receiver and more generally on the entire
+   * execution context. This method <em>should</em> be called by
+   * widget implementors to enforce the standard SWT invariants.
+   * <p>
+   * Currently, it is an error to invoke any method (other than
+   * <code>isDisposed()</code>) on a widget that has had its
+   * <code>dispose()</code> method called. It is also an error
+   * to call widget methods from any thread that is different
+   * from the thread that created the widget.
+   * </p><p>
+   * In future releases of SWT, there may be more or fewer error
+   * checks and exceptions may be thrown for different reasons.
+   * </p>
+   *
+   * @exception SWTException <ul>
+   *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
+   *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
+   * </ul>
+   */
+  protected void checkWidget() {
+    if( display == null ) {
+      DND.error( SWT.ERROR_WIDGET_DISPOSED );
+    }
+    if( display.getThread() != Thread.currentThread() ) {
+      DND.error( SWT.ERROR_THREAD_INVALID_ACCESS );
+    }
+    if( display.isDisposed() ) {
+      DND.error( SWT.ERROR_WIDGET_DISPOSED );
+    }
+  }
+
+  /**
+   * Checks that this class can be subclassed.
+   * <p>
+   * The SWT class library is intended to be subclassed
+   * only at specific, controlled points. This method enforces this
+   * rule unless it is overridden.
+   * </p><p>
+   * <em>IMPORTANT:</em> By providing an implementation of this
+   * method that allows a subclass of a class which does not
+   * normally allow subclassing to be created, the implementer
+   * agrees to be fully responsible for the fact that any such
+   * subclass will likely fail between SWT releases and will be
+   * strongly platform specific. No support is provided for
+   * user-written classes which are implemented in this fashion.
+   * </p><p>
+   * The ability to subclass outside of the allowed SWT classes
+   * is intended purely to enable those not on the SWT development
+   * team to implement patches in order to get around specific
+   * limitations in advance of when those limitations can be
+   * addressed by the team. Subclassing should not be attempted
+   * without an intimate and detailed understanding of the hierarchy.
+   * </p>
+   *
+   * @exception SWTException <ul>
+   *    <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
+   * </ul>
+   */
+  protected void checkSubclass() {
+    String name = getClass().getName();
+    String validName = Clipboard.class.getName();
+    if( !validName.equals( name ) ) {
+      DND.error( SWT.ERROR_INVALID_SUBCLASS );
+    }
+  }
+
+  private static void checkOperationMode() {
+    if( getApplicationContext().getLifeCycleFactory().getLifeCycle() instanceof SimpleLifeCycle ) {
+      throw new UnsupportedOperationException( "Method not supported in JEE_COMPATIBILITY mode." );
+    }
+  }
+
+  /**
+   * This listener interface is used to inform application code that the result of clipboard
+   * operation execution is available.
+   *
+   * @see Clipboard#readText()
+   */
+  private interface ClipboardListener {
+
+    void operationSucceeded( ClipboardOperation operation, String result );
+
+    void operationFailed( ClipboardOperation operation, String errorMessage );
+
+  }
+
+  private class ClipboardOperationHandler extends AbstractOperationHandler {
+
+    @Override
+    public void handleCall( String methodName, JsonObject properties ) {
+      if( METHOD_OPERATION_SUCCEEDED.equals( methodName ) ) {
+        final ClipboardOperation operation = getOperation( properties );
+        final String result = properties.get( PROP_RESULT ).asString();
+        operationResult = result;
+        if( listener != null ) {
+          ProcessActionRunner.add( new Runnable() {
+            @Override
+            public void run() {
+              listener.operationSucceeded( operation, result );
+            }
+          } );
+        }
+      } else if( METHOD_OPERATION_FAILED.equals( methodName ) ) {
+        final ClipboardOperation operation = getOperation( properties );
+        final String errorMessage = properties.get( PROP_ERROR_MESSAGE ).asString();
+        if( ClipboardOperation.WRITE_TEXT.equals( operation ) ) {
+          operationResult = new SWTException( SWT.ERROR_CANNOT_SET_TEXT, errorMessage );
+        } else if( ClipboardOperation.READ_TEXT.equals( operation ) ) {
+          operationResult = new SWTException( SWT.ERROR_CANNOT_GET_TEXT, errorMessage );
+        } else {
+          operationResult = new SWTException( errorMessage );
+        }
+        if( listener != null ) {
+          ProcessActionRunner.add( new Runnable() {
+            @Override
+            public void run() {
+              listener.operationFailed( operation, errorMessage );
+            }
+          } );
+        }
+      }
+    }
+
+    private ClipboardOperation getOperation( JsonObject properties ) {
+      String operation = properties.get( PROP_OPERATION ).asString();
+      if( METHOD_WRITE_TEXT.equals( operation ) ) {
+        return ClipboardOperation.WRITE_TEXT;
+      } else if( METHOD_READ_TEXT.equals( operation ) ) {
+        return ClipboardOperation.READ_TEXT;
+      } else {
+        return null;
+      }
+    }
+
+  }
+
+}
diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/dnd/DND.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/dnd/DND.java
index a629800..ebbdc92 100644
--- a/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/dnd/DND.java
+++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/dnd/DND.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2009 IBM Corporation and others.
+ * Copyright (c) 2000, 2020 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -26,15 +26,17 @@
 	/**
 	 * The transfer mechanism for data that is being cut
 	 * and then pasted or copied and then pasted (value is 1).
+	 * 
+	 * @see Clipboard
 	 */
-//  * @see Clipboard
 	public final static int CLIPBOARD = 1 << 0;
 	
 	/**
 	 * The transfer mechanism for clients that use the selection 
 	 * mechanism (value is 2).
+	 * 
+	 * @see Clipboard
 	 */
-//	* @see Clipboard
 	public final static int SELECTION_CLIPBOARD = 1 << 1;
 
 	/**
diff --git a/examples/org.eclipse.rap.demo.controls/src/org/eclipse/rap/demo/controls/ClipboardTab.java b/examples/org.eclipse.rap.demo.controls/src/org/eclipse/rap/demo/controls/ClipboardTab.java
new file mode 100644
index 0000000..8dd76a9
--- /dev/null
+++ b/examples/org.eclipse.rap.demo.controls/src/org/eclipse/rap/demo/controls/ClipboardTab.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * Copyright (c) 2020 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.demo.controls;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTError;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Text;
+
+
+public class ClipboardTab extends ExampleTab {
+  
+  private Clipboard clipboard;
+  private Text copyText;
+  private Text pasteText;
+  
+  public ClipboardTab() {
+    super( "Clipboard" );
+    setHorizontalSashFormWeights( new int[] { 100, 0 } );
+  }
+
+  @Override
+  protected void createStyleControls( Composite parent ) {
+  }
+
+  @Override
+  protected void createExampleControls( Composite parent ) {
+    clipboard = new Clipboard( parent.getDisplay() );
+    
+    configureDisplay( parent.getDisplay() );
+
+    parent.setLayout( new GridLayout( 2, false )  );
+
+    copyText = new Text( parent, SWT.SINGLE | SWT.BORDER );
+    copyText.setLayoutData( new GridData( SWT.FILL, SWT.CENTER, true, false ) );
+    Button copyButton = new Button( parent, SWT.PUSH );
+    copyButton.setText( "Copy" );
+    copyButton.addListener( SWT.Selection, new Listener() {
+      @Override
+      public void handleEvent( Event event ) {
+        String text = copyText.getText();
+        if( !text.isEmpty() ) {
+          setClipboardData( text );
+        }
+      }
+    } );
+
+    pasteText = new Text( parent, SWT.SINGLE | SWT.BORDER );
+    pasteText.setLayoutData( new GridData( SWT.FILL, SWT.CENTER, true, false ) );
+    Button pasteButton = new Button( parent, SWT.PUSH );
+    pasteButton.setText( "Paste" );
+    pasteButton.addListener( SWT.Selection, new Listener() {
+      @Override
+      public void handleEvent( Event event ) {
+        String text = getClipboardData();
+        pasteText.setText( text );
+      }
+    } );
+
+  }
+  
+  private void configureDisplay( Display display ) {
+    display.setData( RWT.ACTIVE_KEYS, new String[] { "CTRL+C", "CTRL+V" } );
+    display.setData( RWT.CANCEL_KEYS, new String[] { "CTRL+C", "CTRL+V" } );
+    display.addFilter( SWT.KeyDown, new Listener() {
+      @Override
+      public void handleEvent( Event event ) {
+        if( event.character == 'c' ) {
+          String text = copyText.getText();
+          if( !text.isEmpty() ) {
+            setClipboardData( text );
+          }
+        } else if( event.character == 'v' ) {
+          String text = getClipboardData();
+          pasteText.setText( text );
+        }
+      }
+    } );
+  }
+  
+  private void setClipboardData( String textData ) {
+    try {
+      Transfer[] transfers = new Transfer[]{ TextTransfer.getInstance() };
+      Object[] data = new Object[]{ textData };
+      clipboard.setContents( data, transfers );
+    } catch( SWTError error ) {
+      String message = "Unable to set client clipboard data!";
+      MessageDialog.openError( getShell(), "Clipboard Error", message );
+    }
+  }
+  
+  private String getClipboardData() {
+    String textData = ( String )clipboard.getContents( TextTransfer.getInstance() );
+    if( textData == null ) {
+      String message = "Unable to get client clipboard data!";
+      MessageDialog.openError( getShell(), "Clipboard Error", message );
+      return "";
+    }
+    return textData;
+  }
+  
+}
diff --git a/examples/org.eclipse.rap.demo.controls/src/org/eclipse/rap/demo/controls/ControlsDemo.java b/examples/org.eclipse.rap.demo.controls/src/org/eclipse/rap/demo/controls/ControlsDemo.java
index d80895f..6995736 100644
--- a/examples/org.eclipse.rap.demo.controls/src/org/eclipse/rap/demo/controls/ControlsDemo.java
+++ b/examples/org.eclipse.rap.demo.controls/src/org/eclipse/rap/demo/controls/ControlsDemo.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2007, 2016 Innoopract Informationssysteme GmbH and others.
+ * Copyright (c) 2007, 2020 Innoopract Informationssysteme GmbH and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -175,6 +175,7 @@
       new ZOrderTab(),
       new VariantsTab(),
       new ControlDecorationTab(),
+      new ClipboardTab(),
       new ErrorHandlingTab(),
       new ClientServicesTab(),
       new NLSTab(),
diff --git a/tests/org.eclipse.rap.rwt.test/src/org/eclipse/swt/dnd/Clipboard_Test.java b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/swt/dnd/Clipboard_Test.java
new file mode 100644
index 0000000..863b7c4
--- /dev/null
+++ b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/swt/dnd/Clipboard_Test.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ * Copyright (c) 2020 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.swt.dnd;
+
+import static org.eclipse.rap.rwt.application.Application.OperationMode.JEE_COMPATIBILITY;
+import static org.eclipse.rap.rwt.application.Application.OperationMode.SWT_COMPATIBILITY;
+import static org.eclipse.rap.rwt.internal.service.ContextProvider.getApplicationContext;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.rap.json.JsonObject;
+import org.eclipse.rap.rwt.application.Application.OperationMode;
+import org.eclipse.rap.rwt.internal.lifecycle.LifeCycleFactory;
+import org.eclipse.rap.rwt.internal.lifecycle.RWTLifeCycle;
+import org.eclipse.rap.rwt.remote.Connection;
+import org.eclipse.rap.rwt.remote.RemoteObject;
+import org.eclipse.rap.rwt.testfixture.TestContext;
+import org.eclipse.swt.widgets.Display;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class Clipboard_Test {
+  
+  private static final String REMOTE_TYPE = "rwt.client.Clipboard";
+  private Connection connection;
+  private RemoteObject remoteObject;
+  
+  @Rule
+  public TestContext context = new TestContext();
+  
+  private Clipboard clipboard;
+  
+  @Before
+  public void setUp() {
+    Display display = new Display();
+    connection = mock( Connection.class );
+    remoteObject = mock( RemoteObject.class );
+    when( connection.createRemoteObject( anyString() ) ).thenReturn( remoteObject );
+    context.replaceConnection( connection );
+    clipboard = new Clipboard( display );
+  }
+  
+  @Test
+  public void testConstructor_createsRemoteObject() {
+    verify( connection ).createRemoteObject( eq( REMOTE_TYPE ) );
+  }
+  
+  @Test
+  public void testWriteText() {
+    clipboard.writeText( "foo" );
+    
+    verify( remoteObject ).call( "writeText", new JsonObject().add( "text", "foo" ) );
+  }
+  
+  @Test
+  public void testReadText() {
+    clipboard.readText();
+    
+    verify( remoteObject ).call( "readText", null );
+  }
+  
+  @Test
+  public void testSetContents_JEE_COMPATIBILITY() {
+    ensureOperationMode( JEE_COMPATIBILITY );
+    try {
+      Object[] data = new Object[] { "foo" };
+      Transfer[] transfers = new Transfer[] { TextTransfer.getInstance() };
+      clipboard.setContents( data, transfers );
+      fail();
+    } catch( UnsupportedOperationException expected ) {
+      assertEquals( "Method not supported in JEE_COMPATIBILITY mode.", expected.getMessage() );
+    }
+  }
+  
+  @Test( expected = IllegalArgumentException.class )
+  public void testSetContents_nonTextTransfer() {
+    ensureOperationMode( SWT_COMPATIBILITY );
+    
+    Object[] data = new Object[] { "foo" };
+    Transfer[] transfers = new Transfer[] { ImageTransfer.getInstance() };
+    clipboard.setContents( data, transfers );
+  }
+  
+  @Test( expected = IllegalArgumentException.class )
+  public void testSetContents_multipleTransfers() {
+    ensureOperationMode( SWT_COMPATIBILITY );
+    
+    Object[] data = new Object[] { "foo", "bar" };
+    Transfer[] transfers = new Transfer[] { 
+      TextTransfer.getInstance(), 
+      TextTransfer.getInstance() 
+    };
+    clipboard.setContents( data, transfers );
+  }
+  
+  @Test
+  public void testGetContents_JEE_COMPATIBILITY() {
+    ensureOperationMode( JEE_COMPATIBILITY );
+    try {
+      clipboard.getContents( TextTransfer.getInstance() );
+      fail();
+    } catch( UnsupportedOperationException expected ) {
+      assertEquals( "Method not supported in JEE_COMPATIBILITY mode.", expected.getMessage() );
+    }
+  }
+  
+  @Test( expected = IllegalArgumentException.class )
+  public void testGetContents_nonTextTransfer() {
+    ensureOperationMode( SWT_COMPATIBILITY );
+    
+    clipboard.getContents( ImageTransfer.getInstance() );
+  }
+  
+  @Test
+  public void testDispose() {
+    clipboard.dispose();
+    
+    assertTrue( clipboard.isDisposed() );
+  }
+  
+  private static void ensureOperationMode( OperationMode operationMode ) {
+    LifeCycleFactory lifeCycleFactory = getApplicationContext().getLifeCycleFactory();
+    lifeCycleFactory.deactivate();
+    if( SWT_COMPATIBILITY.equals( operationMode ) ) {
+      lifeCycleFactory.configure( RWTLifeCycle.class );
+    }
+    lifeCycleFactory.activate();
+  }
+  
+}