You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2021/01/02 23:55:01 UTC

[incubator-hop] branch master updated: HOP-2386: Add RAP fragment to the master branch

This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-hop.git


The following commit(s) were added to refs/heads/master by this push:
     new a7d18b2  HOP-2386: Add RAP fragment to the master branch
     new edeb72a  Merge pull request #526 from HiromuHota/HOP-2386
a7d18b2 is described below

commit a7d18b2f6e832461f51e1313d805e1c7a00c9df6
Author: Hiromu Hota <hi...@gmail.com>
AuthorDate: Sat Jan 2 10:42:31 2021 -0800

    HOP-2386: Add RAP fragment to the master branch
---
 pom.xml                                            |   1 +
 rap/pom.xml                                        |  51 +++++
 .../apache/hop/ui/core/gui/GuiResourceImpl.java    |  27 +++
 .../org/apache/hop/ui/hopgui/CanvasFacadeImpl.java | 168 +++++++++++++++++
 .../apache/hop/ui/hopgui/CanvasListenerImpl.java   |  46 +++++
 .../java/org/apache/hop/ui/hopgui/Clipboard.java   | 114 ++++++++++++
 .../apache/hop/ui/hopgui/ClipboardListener.java    |  33 ++++
 .../hop/ui/hopgui/DownloadServiceHandler.java      |  60 ++++++
 .../java/org/apache/hop/ui/hopgui/HopGuiImpl.java  |  26 +++
 .../main/java/org/apache/hop/ui/hopgui/HopWeb.java |  58 ++++++
 .../org/apache/hop/ui/hopgui/HopWebEntryPoint.java |  75 ++++++++
 .../ui/hopgui/HopWebServletContextListener.java    |  62 +++++++
 .../hop/ui/hopgui/ServerPushSessionFacadeImpl.java |  34 ++++
 .../hop/ui/hopgui/TextSizeUtilFacadeImpl.java      |  31 ++++
 .../hop/ui/hopgui/context/GuiContextUtilImpl.java  |  27 +++
 .../org/eclipse/jface/window/DefaultToolTip.java   |  43 +++++
 .../java/org/eclipse/jface/window/ToolTip.java     |  81 ++++++++
 .../resources/org/apache/hop/ui/hopgui/canvas.js   | 206 +++++++++++++++++++++
 .../org/apache/hop/ui/hopgui/clipboard.js          | 149 +++++++++++++++
 19 files changed, 1292 insertions(+)

diff --git a/pom.xml b/pom.xml
index f42e0fa..e556af7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -887,6 +887,7 @@
                 <module>core</module>
                 <module>engine</module>
                 <module>ui</module>
+                <module>rap</module>
                 <module>rcp</module>
                 <module>plugins</module>
             </modules>
diff --git a/rap/pom.xml b/rap/pom.xml
new file mode 100644
index 0000000..c2d12f3
--- /dev/null
+++ b/rap/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.hop</groupId>
+  <artifactId>hop-ui-rap</artifactId>
+  <packaging>jar</packaging>
+  <version>0.60-SNAPSHOT</version>
+
+  <name>Hop GUI (RAP fragment)</name>
+
+  <parent>
+    <groupId>org.apache.hop</groupId>
+    <artifactId>hop</artifactId>
+    <version>0.60-SNAPSHOT</version>
+  </parent>
+
+  <properties>
+    <!-- webSpoon specific dependencies -->
+    <rap.version>3.13.0</rap.version>
+  </properties>
+
+
+  <dependencies>
+    <!-- Hop dependencies -->
+    <dependency>
+      <groupId>org.apache.hop</groupId>
+      <artifactId>hop-ui</artifactId>
+      <version>${project.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.eclipse.platform</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+
+    <!-- Third-party dependencies -->
+    <dependency>
+      <groupId>org.eclipse.rap</groupId>
+      <artifactId>org.eclipse.rap.rwt</artifactId>
+      <version>${rap.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.rap</groupId>
+      <artifactId>org.eclipse.rap.jface</artifactId>
+      <version>${rap.version}</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/rap/src/main/java/org/apache/hop/ui/core/gui/GuiResourceImpl.java b/rap/src/main/java/org/apache/hop/ui/core/gui/GuiResourceImpl.java
new file mode 100644
index 0000000..e7e0f3f
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/core/gui/GuiResourceImpl.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.core.gui;
+
+import org.apache.hop.ui.hopgui.ISingletonProvider;
+import org.eclipse.rap.rwt.SingletonUtil;
+
+public class GuiResourceImpl implements ISingletonProvider {
+  public Object getInstanceInternal() {
+    return SingletonUtil.getSessionInstance( GuiResource.class );
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasFacadeImpl.java b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasFacadeImpl.java
new file mode 100644
index 0000000..4bef25a
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasFacadeImpl.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import org.apache.hop.base.AbstractMeta;
+import org.apache.hop.core.SwtUniversalImage;
+import org.apache.hop.pipeline.PipelineHopMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
+import org.apache.hop.workflow.WorkflowHopMeta;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.eclipse.rap.json.JsonArray;
+import org.eclipse.rap.json.JsonObject;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Display;
+
+public class CanvasFacadeImpl extends CanvasFacade {
+
+  @Override
+  void setDataInternal( Canvas canvas, float magnification, AbstractMeta meta, Class type ) {
+    setDataCommon( canvas, magnification, meta );
+    if ( type == HopGuiWorkflowGraph.class ) {
+      setDataWorkflow( canvas, magnification, meta );
+    } else {
+      setDataPipeline( canvas, magnification, meta );
+    }
+  }
+
+  private void setDataCommon ( Canvas canvas, float magnification, AbstractMeta meta ) {
+    JsonObject jsonProps = new JsonObject();
+    jsonProps.add( "gridsize", PropsUi.getInstance().isShowCanvasGridEnabled() ? PropsUi.getInstance().getCanvasGridSize() : 1 );
+    jsonProps.add( "iconsize", PropsUi.getInstance().getIconSize() );
+    jsonProps.add( "magnification", magnification );
+    canvas.setData( "props", jsonProps );
+
+    JsonArray jsonNotes = new JsonArray();
+    meta.getNotes().forEach( note -> {
+      JsonObject jsonNote = new JsonObject();
+      jsonNote.add( "x", note.getLocation().x );
+      jsonNote.add( "y", note.getLocation().y );
+      jsonNote.add( "width", note.getWidth() );
+      jsonNote.add( "height", note.getHeight() );
+      jsonNote.add( "selected", note.isSelected() );
+      jsonNote.add( "note", note.getNote() );
+      jsonNotes.add( jsonNote );
+    } );
+    canvas.setData( "notes", jsonNotes );
+  }
+  
+  private void setDataWorkflow ( Canvas canvas, float magnification, AbstractMeta meta ) {
+    final int iconSize = HopGui.getInstance().getProps().getIconSize();
+
+    WorkflowMeta workflowMeta = (WorkflowMeta) meta;
+    JsonObject jsonNodes = new JsonObject();
+    workflowMeta.getActions().forEach( actionMeta -> {
+      JsonObject jsonNode = new JsonObject();
+      jsonNode.add( "x", actionMeta.getLocation().x );
+      jsonNode.add( "y", actionMeta.getLocation().y );
+      jsonNode.add( "selected", actionMeta.isSelected() );
+
+      // Translated from org.apache.hop.ui.hopgui.shared.SwtGc.drawActionIcon(int, int, ActionMeta, float)
+      SwtUniversalImage swtImage = null;
+
+      if ( actionMeta.isMissing() ) {
+        swtImage = GuiResource.getInstance().getSwtImageMissing();
+      }
+      else {
+        String pluginId = actionMeta.getAction().getPluginId();
+        if ( pluginId != null ) {
+          swtImage = GuiResource.getInstance().getImagesActions().get( pluginId );
+        }
+      }
+
+      if ( swtImage == null ) {
+        return;
+      }
+
+      int w = Math.round( iconSize * magnification );
+      int h = Math.round( iconSize * magnification );
+      Image image = swtImage.getAsBitmapForSize( Display.getCurrent(), w, h );
+      // Translated
+
+      jsonNode.add( "img",  image.internalImage.getResourceName() );
+      jsonNodes.add( actionMeta.getName(), jsonNode );
+    } );
+    canvas.setData( "nodes", jsonNodes );
+
+    JsonArray jsonHops = new JsonArray();
+    for ( int i = 0; i < workflowMeta.nrWorkflowHops(); i++ ) {
+      JsonObject jsonHop = new JsonObject();
+      WorkflowHopMeta hop = workflowMeta.getWorkflowHop( i );
+      jsonHop.add( "from", hop.getFromAction().getName() );
+      jsonHop.add( "to", hop.getToAction().getName() );
+      jsonHops.add( jsonHop );
+    }
+    canvas.setData( "hops", jsonHops );
+  }
+
+  private void setDataPipeline( Canvas canvas, float magnification, AbstractMeta meta ) {
+    final int iconSize = HopGui.getInstance().getProps().getIconSize();
+
+    PipelineMeta pipelineMeta = (PipelineMeta) meta;
+    JsonObject jsonNodes = new JsonObject();
+    pipelineMeta.getTransforms().forEach( transformMeta -> {
+      JsonObject jsonNode = new JsonObject();
+      jsonNode.add( "x", transformMeta.getLocation().x );
+      jsonNode.add( "y", transformMeta.getLocation().y );
+      jsonNode.add( "selected", transformMeta.isSelected() );
+
+      // Translated from org.apache.hop.ui.hopgui.shared.SwtGc.drawTransformIcon(int, int, TransformMeta, float)
+      SwtUniversalImage swtImage = null;
+
+      if ( transformMeta.isMissing() ) {
+        swtImage = GuiResource.getInstance().getSwtImageMissing();
+      } else if ( transformMeta.isDeprecated() ) {
+        swtImage = GuiResource.getInstance().getSwtImageDeprecated();
+      } else {
+        String pluginId = transformMeta.getPluginId();
+        if ( pluginId != null ) {
+          swtImage = GuiResource.getInstance().getImagesTransforms().get( pluginId );
+        }
+      }
+
+      if ( swtImage == null ) {
+        return;
+      }
+
+      int w = Math.round( iconSize * magnification );
+      int h = Math.round( iconSize * magnification );
+      Image image = swtImage.getAsBitmapForSize( Display.getCurrent(), w, h );
+      // Translated
+
+      jsonNode.add( "img",  image.internalImage.getResourceName() );
+      jsonNodes.add( transformMeta.getName(), jsonNode );
+    } );
+    canvas.setData( "nodes", jsonNodes );
+
+    JsonArray jsonHops = new JsonArray();
+    for ( int i = 0; i < pipelineMeta.nrPipelineHops(); i++ ) {
+      JsonObject jsonHop = new JsonObject();
+      PipelineHopMeta hop = pipelineMeta.getPipelineHop( i );
+      if ( hop.getFromTransform() != null && hop.getToTransform() != null ) {
+        jsonHop.add( "from", hop.getFromTransform().getName() );
+        jsonHop.add( "to", hop.getToTransform().getName() );
+        jsonHops.add( jsonHop );
+      }
+    }
+    canvas.setData( "hops", jsonHops );
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasListenerImpl.java b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasListenerImpl.java
new file mode 100644
index 0000000..c5d2983
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasListenerImpl.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import java.io.IOException;
+import org.apache.commons.io.IOUtils;
+import org.eclipse.rap.rwt.SingletonUtil;
+import org.eclipse.rap.rwt.scripting.ClientListener;
+
+public class CanvasListenerImpl extends ClientListener implements ISingletonProvider{
+
+  @Override
+  public Object getInstanceInternal() {
+    return SingletonUtil.getSessionInstance( CanvasListenerImpl.class );
+  }
+
+  protected CanvasListenerImpl() {
+    super( getText() );
+  }
+
+  private static String getText() {
+    String canvasScript = null;
+    try {
+      canvasScript = IOUtils.toString( CanvasListenerImpl.class.getResourceAsStream( "canvas.js" ) );
+    } catch ( IOException e1 ) {
+      // TODO Auto-generated catch block
+      e1.printStackTrace();
+    }
+    return canvasScript;
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/Clipboard.java b/rap/src/main/java/org/apache/hop/ui/hopgui/Clipboard.java
new file mode 100644
index 0000000..09afe53
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/Clipboard.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.rap.json.JsonObject;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.client.service.ClientFileLoader;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+import org.eclipse.rap.rwt.remote.AbstractOperationHandler;
+import org.eclipse.rap.rwt.remote.Connection;
+import org.eclipse.rap.rwt.remote.RemoteObject;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.rap.rwt.widgets.WidgetUtil;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Widget;
+
+/**
+ * Clipboard implemented as a custom widget. This class is meant to be instantiated only once per UI session.
+ */
+public class Clipboard extends Widget {
+
+  private RemoteObject remoteObject;
+  private List<ClipboardListener> listeners = new ArrayList<ClipboardListener>();
+
+  public Clipboard( Composite parent ) {
+    super( parent, SWT.NONE );
+    ResourceManager resourceManager = RWT.getResourceManager();
+    ClientFileLoader clientFileLoader = RWT.getClient().getService( ClientFileLoader.class );
+    clientFileLoader.requireJs( resourceManager.getLocation( "js/clipboard.js" ) );
+
+    Connection connection = RWT.getUISession().getConnection();
+    remoteObject = connection.createRemoteObject( "webSpoon.Clipboard" );
+    remoteObject.set( "parent", WidgetUtil.getId( this ) );
+    remoteObject.set( "self", remoteObject.getId() );
+    remoteObject.setHandler( new AbstractOperationHandler() {
+      @Override
+      public void handleNotify(String event, JsonObject properties) {
+        String widgetId = properties.get( "widgetId" ).asString();
+        if ( event.equals( "paste" ) ) {
+          listeners.stream()
+            .filter( l -> l.getWidgetId().equals( widgetId ) )
+            .forEach( l -> l.pasteListener( properties.get( "text" ).asString() ) );
+        } else if ( event.equals( "cut" ) ) {
+          listeners.stream()
+            .filter( l -> l.getWidgetId().equals( widgetId ) )
+            .forEach( l -> l.cutListener() );
+        }
+      }
+    } );
+    remoteObject.listen( "paste", true );
+    remoteObject.listen( "copy", true );
+    remoteObject.listen( "cut", true );
+  }
+
+  public void dispose() {
+    remoteObject.destroy();
+  }
+
+  /**
+   * Set data to the clipboard. This should be called before cut/copy.
+   * @param text
+   */
+  public void setContents( String text ) {
+    remoteObject.set( "text", text );
+  }
+
+  /**
+   * Attach a widget to the clipboard. This should be called before any clipboard event.
+   * @param widget
+   */
+  public void attachToClipboard( Widget widget ) {
+    String widgetId = WidgetUtil.getId( widget );
+    JavaScriptExecutor executor = RWT.getClient().getService( JavaScriptExecutor.class );
+    executor.execute(
+      "var x = document.getElementById( 'input-clipboard' );\n"
+      + "x.value='" + widgetId + "';\n"
+      + "x.focus();"
+    );
+  }
+
+  public void addClipboardListener( ClipboardListener listener ) {
+    this.listeners.add( listener );
+  }
+
+  public void removeClipboardListener( ClipboardListener listener ) {
+    this.listeners.remove( listener );
+  }
+
+  public void downloadCanvasImage( String rwtId, String name ) {
+    JsonObject obj = new JsonObject();
+    obj.add( "rwtId", rwtId );
+    obj.add( "name", name );
+    remoteObject.call( "downloadCanvasImage", obj );
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/ClipboardListener.java b/rap/src/main/java/org/apache/hop/ui/hopgui/ClipboardListener.java
new file mode 100644
index 0000000..c7a136f
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/ClipboardListener.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+public interface ClipboardListener {
+
+  /**
+   * Return the Id of the associated widget.
+   * See {@link org.eclipse.rap.rwt.widgets.WidgetUtil#getId(Widget)}.
+   * @return
+   */
+  String getWidgetId();
+
+  void pasteListener( String text );
+
+  void cutListener();
+
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/DownloadServiceHandler.java b/rap/src/main/java/org/apache/hop/ui/hopgui/DownloadServiceHandler.java
new file mode 100644
index 0000000..4a9bce2
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/DownloadServiceHandler.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.rap.rwt.service.ServiceHandler;
+
+public class DownloadServiceHandler implements ServiceHandler {
+
+  public void service( HttpServletRequest request, HttpServletResponse response )
+    throws IOException, ServletException {
+    // Which file to download?
+    String fileName = request.getParameter( "filename" );
+    // Get the file content
+    File file = new File( fileName );
+    FileInputStream fin = null;
+    byte[] download = null;
+    try {
+      fin = new FileInputStream( file );
+      download = new byte[(int) file.length() ];
+      fin.read( download );
+    } catch ( IOException e ) {
+      throw new IOException( e.getMessage() );
+    }
+    fin.close();
+    /*
+     *  TODO : if the following line is not executed, the exported xml file remains in the server.
+     *  There might be a better way to ensure the file is deleted.
+     */
+    file.delete();
+    // Send the file in the response
+    response.setContentType( "application/octet-stream" );
+    response.setContentLength( download.length );
+    String contentDisposition = "attachment; filename=\"" + file.getName() + "\"";
+    response.setHeader( "Content-Disposition", contentDisposition );
+    response.getOutputStream().write( download );
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java b/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
new file mode 100644
index 0000000..d965d88
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import org.eclipse.rap.rwt.SingletonUtil;
+
+public class HopGuiImpl implements ISingletonProvider {
+  public Object getInstanceInternal() {
+    return SingletonUtil.getSessionInstance( HopGui.class );
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
new file mode 100644
index 0000000..14f9445
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FilenameUtils;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.eclipse.rap.rwt.client.WebClient;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+
+public class HopWeb implements ApplicationConfiguration {
+
+  public void configure( Application application ) {
+    application.addResource( "ui/images/hopUi.ico", new ResourceLoader() {
+      public InputStream getResourceAsStream( String resourceName ) throws IOException {
+        return this.getClass().getClassLoader().getResourceAsStream( "ui/images/hopUi.ico" );
+      }
+    } );
+    Arrays.asList(
+      "org/apache/hop/ui/hopgui/clipboard.js"
+    ).stream().forEach( str -> {
+      application.addResource( "js/" + FilenameUtils.getName( str ), new ResourceLoader() {
+        @Override
+        public InputStream getResourceAsStream( String resourceName ) throws IOException {
+          return this.getClass().getClassLoader().getResourceAsStream( str );
+        }
+      } );
+    });
+    Map<String, String> properties = new HashMap<String, String>();
+    properties.put( WebClient.PAGE_TITLE, "Hop" );
+    properties.put( WebClient.FAVICON, "ui/images/hopUi.ico" );
+    application.addEntryPoint( "/ui", HopWebEntryPoint.class, properties );
+    application.setOperationMode( Application.OperationMode.SWT_COMPATIBILITY );
+
+    application.addServiceHandler( "downloadServiceHandler", new DownloadServiceHandler() );
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
new file mode 100644
index 0000000..6e0a9fc
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.extension.ExtensionPointHandler;
+import org.apache.hop.core.extension.HopExtensionPoint;
+import org.apache.hop.ui.core.PropsUi;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.AbstractEntryPoint;
+import org.eclipse.rap.rwt.client.service.StartupParameters;
+import org.eclipse.rap.rwt.widgets.WidgetUtil;
+import org.eclipse.swt.widgets.Composite;
+
+public class HopWebEntryPoint extends AbstractEntryPoint {
+
+  @Override
+  protected void createContents( Composite parent ) {
+    // Transferring Widget Data for client-side canvas drawing instructions
+    WidgetUtil.registerDataKeys( "props" );
+    WidgetUtil.registerDataKeys( "mode" );
+    WidgetUtil.registerDataKeys( "nodes" );
+    WidgetUtil.registerDataKeys( "hops" );
+    WidgetUtil.registerDataKeys( "notes" );
+    /*
+     *  The following lines were migrated from Spoon.main
+     *  because they are session specific.
+     */
+    // Options
+    StartupParameters serviceParams = RWT.getClient().getService( StartupParameters.class );
+    List<String> args = new ArrayList<String>();
+    String[] options = { "rep", "user", "pass", "trans", "job", "dir", "file" };
+    for ( String option : options ) {
+      if ( serviceParams.getParameter( option ) != null ) {
+        args.add( "-" + option + "=" + serviceParams.getParameter( option ) );
+      }
+    }
+
+    // Execute Spoon.createContents
+    HopGui.getInstance().setCommandLineArguments( args );
+    HopGui.getInstance().setShell( parent.getShell() );
+    HopGui.getInstance().setProps( PropsUi.getInstance() );
+    try {
+      ExtensionPointHandler.callExtensionPoint(
+        HopGui.getInstance().getLog(),
+        HopGui.getInstance().getVariables(),
+        HopExtensionPoint.HopGuiInit.id,
+        HopGui.getInstance()
+      );
+    } catch(Exception e) {
+      HopGui.getInstance().getLog().logError( "Error calling extension point plugin(s) for HopGuiInit", e);
+    }
+
+    HopGui.getInstance().open();
+  }
+
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebServletContextListener.java b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebServletContextListener.java
new file mode 100644
index 0000000..2a456b6
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebServletContextListener.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletContextEvent;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.www.HopServerSingleton;
+import org.eclipse.rap.rwt.engine.RWTServletContextListener;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+public class HopWebServletContextListener extends RWTServletContextListener {
+  private final static Logger logger = Logger.getLogger( HopWebServletContextListener.class.getName() );
+
+  public void contextInitialized( ServletContextEvent event ) {
+    /*
+     *  The following lines are from HopGui.main
+     *  because they are application-wide context.
+     */
+    try {
+      HopEnvironment.init();
+      HopGuiEnvironment.init();
+    } catch ( HopException e ) {
+      e.printStackTrace();
+    }
+    super.contextInitialized( event );
+  }
+  public void contextDestroyed( ServletContextEvent event ) {
+    super.contextDestroyed( event );
+    // Kill all remaining things in this VM!
+    System.exit( 0 );
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/ServerPushSessionFacadeImpl.java b/rap/src/main/java/org/apache/hop/ui/hopgui/ServerPushSessionFacadeImpl.java
new file mode 100644
index 0000000..4ed4abe
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/ServerPushSessionFacadeImpl.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import org.eclipse.rap.rwt.SingletonUtil;
+import org.eclipse.rap.rwt.service.ServerPushSession;
+
+public class ServerPushSessionFacadeImpl extends ServerPushSessionFacade {
+
+  @Override
+  void startInternal() {
+    SingletonUtil.getSessionInstance( ServerPushSession.class ).start();
+  }
+
+  @Override
+  void stopInternal() {
+    SingletonUtil.getSessionInstance( ServerPushSession.class ).stop();
+  }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/TextSizeUtilFacadeImpl.java b/rap/src/main/java/org/apache/hop/ui/hopgui/TextSizeUtilFacadeImpl.java
new file mode 100644
index 0000000..b710acd
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/TextSizeUtilFacadeImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import org.eclipse.rap.rwt.internal.textsize.TextSizeUtil;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Display;
+
+public class TextSizeUtilFacadeImpl extends TextSizeUtilFacade {
+
+  @Override
+  Point textExtentInternal(String text) {
+    return TextSizeUtil.stringExtent(Display.getCurrent().getSystemFont(), text);
+  }
+
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/context/GuiContextUtilImpl.java b/rap/src/main/java/org/apache/hop/ui/hopgui/context/GuiContextUtilImpl.java
new file mode 100644
index 0000000..a6d0ec7
--- /dev/null
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/context/GuiContextUtilImpl.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.context;
+
+import org.apache.hop.ui.hopgui.ISingletonProvider;
+import org.eclipse.rap.rwt.SingletonUtil;
+
+public class GuiContextUtilImpl implements ISingletonProvider {
+  public Object getInstanceInternal() {
+    return SingletonUtil.getSessionInstance( GuiContextUtil.class );
+  }
+}
diff --git a/rap/src/main/java/org/eclipse/jface/window/DefaultToolTip.java b/rap/src/main/java/org/eclipse/jface/window/DefaultToolTip.java
new file mode 100644
index 0000000..be4874b
--- /dev/null
+++ b/rap/src/main/java/org/eclipse/jface/window/DefaultToolTip.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.eclipse.jface.window;
+
+import org.apache.hop.ui.core.widget.CheckBoxToolTip;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.widgets.Control;
+
+public class DefaultToolTip extends CheckBoxToolTip {
+
+  public DefaultToolTip( Control control ) {
+    super( control );
+  }
+
+  public DefaultToolTip( Control control, int style, boolean manualActivation ) {
+    super( control );
+  }
+
+  public void setBackgroundColor(Color color) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void setForegroundColor(Color color) {
+    // TODO Auto-generated method stub
+
+  }
+}
diff --git a/rap/src/main/java/org/eclipse/jface/window/ToolTip.java b/rap/src/main/java/org/eclipse/jface/window/ToolTip.java
new file mode 100644
index 0000000..cc12e47
--- /dev/null
+++ b/rap/src/main/java/org/eclipse/jface/window/ToolTip.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.eclipse.jface.window;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+
+public abstract class ToolTip extends org.eclipse.swt.widgets.ToolTip {
+
+  public static final int RECREATE = 1;
+  public static final int NO_RECREATE = 1 << 1;
+
+  public ToolTip( Control control ) {
+    super( control.getShell(), SWT.ICON_INFORMATION );
+  }
+
+  public ToolTip( Control control, int style, boolean manualActivation ) {
+    super( control.getShell(), SWT.ICON_INFORMATION );
+  }
+
+  public void setRespectMonitorBounds(boolean b) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void setRespectDisplayBounds(boolean b) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void setHideDelay(int i) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void setPopupDelay(int i) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void setHideOnMouseDown(boolean b) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void setShift(Point point) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void show(Point location) {
+    super.setLocation( location );
+    super.setVisible( true );
+  }
+
+  public void hide() {
+    super.setVisible( false );
+  }
+
+  protected void afterHideToolTip(Event event) {
+    // TODO Auto-generated method stub
+
+  }
+}
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas.js b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas.js
new file mode 100644
index 0000000..5f26c10
--- /dev/null
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas.js
@@ -0,0 +1,206 @@
+//# sourceURL=canvas.js
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+var x1, x2, y1, y2;
+var clicked = null;
+
+var handleEvent = function( event ) {
+  var mode = event.widget.getData( "mode" );
+  var nodes = event.widget.getData( "nodes" );
+  var hops = event.widget.getData( "hops" );
+  var notes = event.widget.getData( "notes" );
+  var props = event.widget.getData( "props" );
+  var magnification = props.magnification;
+  var gridsize = props.gridsize;
+  var iconsize = props.iconsize;
+
+  switch( event.type ) {
+  case SWT.MouseDown:
+    x1 = event.x / magnification;
+    y1 = event.y / magnification;
+
+    // Determine which node is clicked if any
+    for ( var key in nodes ) {
+      var node = nodes[key];
+      if ( node.x <= x1 && x1 < node.x + iconsize
+        && node.y <= y1 && y1 < node.y + iconsize ) {
+        clicked = node;
+      }
+    }
+    break;
+  case SWT.MouseUp:
+    clicked = null;
+    break;
+  case SWT.MouseMove:
+    x2 = event.x / magnification;
+    y2 = event.y / magnification;
+    if ( mode == null ) {
+      break;
+    }
+    if ( mode != "null" ) {
+      event.widget.redraw();
+    }
+    break;
+  case SWT.Paint:
+    // Client-side does not redraw when first-drawing (null) and after mouseup ("null")
+    if ( mode == null || mode == "null" ) {
+      break;
+    }
+    var gc = event.gc;
+    var dx = x2 - x1;
+    var dy = y2 - y1;
+
+    // Clear the canvas
+    gc.clearRect( 0, 0, gc.canvas.width / magnification, gc.canvas.height / magnification );
+
+    // Draw grids
+    if ( gridsize > 1 ) {
+      gc.fillStyle = 'black';
+      gc.beginPath();
+      gc.setLineDash( [ 1, gridsize - 1 ] );
+      // vertical grid
+      for ( var i = gridsize; i < gc.canvas.width / magnification; i += gridsize ) {
+        gc.moveTo( i, 0 );
+        gc.lineTo( i, gc.canvas.height / magnification );
+      }
+      // horizontal grid
+      for ( var j = gridsize; j < gc.canvas.height / magnification; j += gridsize ) {
+        gc.moveTo( 0, j );
+        gc.lineTo( gc.canvas.width / magnification, j );
+      }
+      gc.stroke();
+      gc.setLineDash( [] );
+      gc.fillStyle = 'white';
+    }
+
+    // Draw hops
+    hops.forEach( function ( hop ) {
+      gc.beginPath();
+      if ( mode == "drag" && nodes[hop.from].selected ) {
+        gc.moveTo( snapToGrid( nodes[hop.from].x + dx, gridsize ) + iconsize / 2,
+                   snapToGrid( nodes[hop.from].y + dy, gridsize ) + iconsize / 2 );
+      } else {
+        gc.moveTo( nodes[hop.from].x + iconsize / 2, nodes[hop.from].y + iconsize / 2 );
+      }
+      if ( mode == "drag" && nodes[hop.to].selected ) {
+        gc.lineTo( snapToGrid( nodes[hop.to].x + dx, gridsize ) + iconsize / 2,
+                   snapToGrid( nodes[hop.to].y + dy, gridsize ) + iconsize / 2 );
+      } else {
+        gc.lineTo( nodes[hop.to].x + iconsize / 2, nodes[hop.to].y + iconsize / 2 );
+      }
+      gc.stroke();
+    } );
+
+    for ( var key in nodes ) {
+      var node = nodes[key];
+      var x = node.x;
+      var y = node.y;
+
+      // Move selected nodes
+      if ( mode == "drag" && ( node.selected || node == clicked ) ) {
+        x = snapToGrid( node.x + dx, gridsize );
+        y = snapToGrid( node.y + dy, gridsize );
+      }
+      // Draw a icon background
+      gc.fillRect( x, y, iconsize, iconsize );
+
+      // Draw node icon
+      var img = new Image();
+      img.src = 'rwt-resources/' + node.img;
+      gc.drawImage( img, x, y, iconsize, iconsize );
+
+      // Draw a bounding rectangle
+      if ( node.selected || node == clicked ) {
+        gc.lineWidth = 3;
+        gc.strokeStyle = 'rgb(0, 93, 166)';
+      } else {
+        gc.strokeStyle = 'rgb(61, 99, 128)'; //colorCrystalTextPentaho
+      }
+      drawRoundRectangle( gc, x - 1, y - 1, iconsize + 1, iconsize + 1, 8, 8, false );
+      gc.strokeStyle = 'black';
+      gc.lineWidth = 1;
+
+      // Draw node name
+//      gc.fillStyle = 'black';
+//      gc.fillText( key, x + iconsize / 2 - gc.measureText(key).width / 2, y + iconsize + 7 );
+//      gc.fillStyle = 'white';
+    }
+
+    // Draw notes
+    notes.forEach( function ( note ) {
+      gc.beginPath();
+      // margin = 10 see org.pentaho.di.core.gui.BasePainter.drawNote(NotePadMeta)
+      if ( mode == "drag" && note.selected ) {
+        gc.rect( snapToGrid( note.x + dx, gridsize), snapToGrid( note.y + dy, gridsize), note.width + 10, note.height + 10 );
+      } else {
+        gc.rect( note.x, note.y, note.width + 10, note.height + 10 );
+      }
+      gc.stroke();
+    } );
+
+    // Draw a new hop
+    if ( mode == "hop" && clicked ) {
+      gc.beginPath();
+      gc.moveTo( clicked.x + iconsize / 2, clicked.y + iconsize / 2 );
+      gc.lineTo( x2, y2 );
+      gc.stroke();
+    }
+
+    // Draw a selection rectangle
+    if ( mode == "select" ) {
+      gc.beginPath();
+      gc.rect( x1, y1, dx, dy );
+      gc.stroke();
+    }
+    break;
+  }
+};
+
+function snapToGrid( x, gridsize ) {
+  return gridsize * Math.floor( x / gridsize );
+}
+
+/*
+ * Port of GCOperationWriter#drawRoundRectangle
+ */
+function drawRoundRectangle( gc, x, y, w, h, arcWidth, arcHeight, fill ) {
+  var offset = 0;
+  if( !fill && gc.lineWidth % 2 != 0 ) {
+    offset = 0.5;
+  }
+  x = x + offset;
+  y = y + offset;
+  var rx = arcWidth / 2 + 1;
+  var ry = arcHeight / 2 + 1;
+  gc.beginPath();
+  gc.moveTo( x, y + ry );
+  gc.lineTo( x, y + h - ry );
+  gc.quadraticCurveTo( x, y + h, x + rx, y + h );
+  gc.lineTo( x + w - rx, y + h );
+  gc.quadraticCurveTo( x + w, y + h, x + w, y + h - ry );
+  gc.lineTo( x + w, y + ry );
+  gc.quadraticCurveTo( x + w, y, x + w - rx, y );
+  gc.lineTo( x + rx, y );
+  gc.quadraticCurveTo( x, y, x, y + ry );
+  if ( fill ) {
+    gc.fill();
+  } else {
+    gc.stroke();
+  }
+}
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/clipboard.js b/rap/src/main/resources/org/apache/hop/ui/hopgui/clipboard.js
new file mode 100644
index 0000000..db3717d
--- /dev/null
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/clipboard.js
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use strict";
+
+(function(){
+  'use strict';
+
+  rap.registerTypeHandler( "webSpoon.Clipboard", {
+      factory : function( properties ) {
+        this._widget = new webSpoon.Clipboard( properties );
+        if ( rwt.client.Client._browserName === 'explorer' ) {
+          var doc = rwt.widgets.base.ClientDocument.getInstance();
+          doc.addEventListener( "keydown", this._widget._onKeyDown, this._widget );
+        }
+        return this._widget;
+      },
+      destructor : function () {
+        if ( rwt.client.Client._browserName === 'explorer' ) {
+          var doc = rwt.widgets.base.ClientDocument.getInstance();
+          doc.removeEventListener( "keydown", this._widget._onKeyDown, this._widget );
+        }
+      },
+      properties : [ "text" ],
+      events : [ "paste", "copy", "cut" ],
+      methods : [ "downloadCanvasImage" ]
+  } );
+
+  rwt.define( "webSpoon" );
+
+  webSpoon.Clipboard = function ( properties ) {
+    var x = document.createElement("INPUT");
+    x.setAttribute( "id", "input-clipboard" );
+    x.setAttribute( "remoteObjectId", properties.self );
+    x.addEventListener( "paste", function( event ) {
+      var obj = rap.getObject( this.getAttribute( 'remoteObjectid' ) );
+      var remoteObject = rap.getRemoteObject( obj );
+      var text = '';
+      if ( typeof event.clipboardData === 'undefined' ) {
+        text = window.clipboardData.getData( 'Text' ); // IE
+      } else {
+        text = event.clipboardData.getData( 'text/plain' );
+      }
+      event.preventDefault();
+      remoteObject.notify( "paste", { "text": text, "widgetId": event.target.value } );
+      console.log('paste');
+    }, this );
+    x.addEventListener( "copy", function( event ) {
+      var obj = rap.getObject( this.getAttribute( 'remoteObjectid' ) );
+      var remoteObject = rap.getRemoteObject( obj );
+      if ( typeof event.clipboardData === 'undefined' ) {
+        window.clipboardData.setData( 'Text', obj.getText() ); // IE
+      } else {
+        event.clipboardData.setData( 'text/plain', obj.getText() );
+      }
+      event.preventDefault();
+      if ( rwt.client.Client._browserName != 'explorer' ) {
+        remoteObject.notify( "copy", { "widgetId": event.target.value } );
+        console.log('copy');
+      }
+    }, this );
+    x.addEventListener( "cut", function( event ) {
+      var obj = rap.getObject( this.getAttribute( 'remoteObjectid' ) );
+      var remoteObject = rap.getRemoteObject( obj );
+      if ( typeof event.clipboardData === 'undefined' ) {
+        window.clipboardData.setData( 'Text', obj.getText() ); // IE
+      } else {
+        event.clipboardData.setData( 'text/plain', obj.getText() );
+      }
+      event.preventDefault();
+      remoteObject.notify( "cut", { "widgetId": event.target.value } );
+      console.log('cut');
+    }, this );
+    document.body.appendChild( x );
+  };
+
+  webSpoon.Clipboard.prototype = {
+    setText : function( text ) {
+      this._text = text;
+    },
+
+    getText : function() {
+      return this._text;
+    },
+
+    downloadCanvasImage : function( obj ) {
+      var i = 0;
+      for ( ; i < document.getElementsByTagName( 'canvas' ).length; i++ ) {
+        if ( document.getElementsByTagName( 'canvas' )[ i ].rwtObject._rwtId === obj.rwtId + ".gc" ) {
+          break;
+        }
+      }
+      var canvas = document.getElementsByTagName( 'canvas' )[ i ];
+      if ( window.navigator && window.navigator.msSaveOrOpenBlob ) { // For IE and Edge
+        var blob = canvas.msToBlob();
+        window.navigator.msSaveOrOpenBlob( blob, obj.name + ".png" );
+      } else {
+        var data = canvas.toDataURL();
+        var link = document.createElement( 'a' )
+        link.setAttribute( 'download', obj.name + ".png" );
+        link.setAttribute( 'href', data.replace( "image/png", "image/octet-stream" ) );
+        document.body.appendChild( link );
+        link.click();
+        document.body.removeChild( link );
+      }
+    },
+
+    _onKeyDown : function( event ) {
+      const keyName = event._valueDomEvent.key;
+
+      if ( keyName === 'Control' ) {
+        return;
+      }
+
+      if ( event._valueDomEvent.ctrlKey ) {
+        var x = document.getElementById( "input-clipboard" );
+        x.select();
+        var remoteObject = rap.getRemoteObject( this );
+        if ( keyName === 'c' ) {
+          document.execCommand( 'copy' );
+          remoteObject.notify( "copy", { "widgetId": x.value } );
+          console.log('copy');
+        } else if ( keyName === 'x' ) {
+          /*
+           * cut event cannot be invoked programmatically on IE11 for some reason,
+           * so capture ctrl+x then execute copy (instead of cut) and notify cut.
+           */
+          document.execCommand( 'copy' );
+          remoteObject.notify( "cut", { "widgetId": x.value } );
+          console.log('cut');
+        }
+      }
+    }
+  };
+}());