//CHECKSTYLE:FileLength:OFF
/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2018 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed 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.pentaho.di.ui.spoon.trans;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.window.DefaultToolTip;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Widget;
import org.pentaho.di.core.CheckResultInterface;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.EngineMetaInterface;
import org.pentaho.di.core.NotePadMeta;
import org.pentaho.di.core.Props;
import org.pentaho.di.core.SwtUniversalImage;
import org.pentaho.di.core.dnd.DragAndDropContainer;
import org.pentaho.di.core.dnd.XMLTransfer;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettleStepException;
import org.pentaho.di.core.exception.KettleValueException;
import org.pentaho.di.core.extension.ExtensionPointHandler;
import org.pentaho.di.core.extension.KettleExtensionPoint;
import org.pentaho.di.core.gui.AreaOwner;
import org.pentaho.di.core.gui.AreaOwner.AreaType;
import org.pentaho.di.core.gui.BasePainter;
import org.pentaho.di.core.gui.GCInterface;
import org.pentaho.di.core.gui.Point;
import org.pentaho.di.core.gui.Redrawable;
import org.pentaho.di.core.gui.SnapAllignDistribute;
import org.pentaho.di.core.logging.DefaultLogLevel;
import org.pentaho.di.core.logging.HasLogChannelInterface;
import org.pentaho.di.core.logging.KettleLogStore;
import org.pentaho.di.core.logging.KettleLoggingEvent;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.logging.LogMessage;
import org.pentaho.di.core.logging.LogParentProvidedInterface;
import org.pentaho.di.core.logging.LoggingObjectInterface;
import org.pentaho.di.core.logging.LoggingObjectType;
import org.pentaho.di.core.logging.LoggingRegistry;
import org.pentaho.di.core.logging.SimpleLoggingObject;
import org.pentaho.di.core.plugins.PluginInterface;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.core.plugins.StepPluginType;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.job.Job;
import org.pentaho.di.job.JobMeta;
import org.pentaho.di.lineage.TransDataLineage;
import org.pentaho.di.repository.KettleRepositoryLostException;
import org.pentaho.di.repository.Repository;
import org.pentaho.di.repository.RepositoryObjectType;
import org.pentaho.di.repository.RepositoryOperation;
import org.pentaho.di.shared.SharedObjects;
import org.pentaho.di.trans.DatabaseImpact;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransExecutionConfiguration;
import org.pentaho.di.trans.TransHopMeta;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.TransPainter;
import org.pentaho.di.trans.TransSupplier;
import org.pentaho.di.trans.debug.BreakPointListener;
import org.pentaho.di.trans.debug.StepDebugMeta;
import org.pentaho.di.trans.debug.TransDebugMeta;
import org.pentaho.di.trans.debug.TransDebugMetaWrapper;
import org.pentaho.di.trans.step.RemoteStep;
import org.pentaho.di.trans.step.RowDistributionInterface;
import org.pentaho.di.trans.step.RowDistributionPluginType;
import org.pentaho.di.trans.step.RowListener;
import org.pentaho.di.trans.step.StepErrorMeta;
import org.pentaho.di.trans.step.StepIOMetaInterface;
import org.pentaho.di.trans.step.StepInterface;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.di.trans.step.StepMetaDataCombi;
import org.pentaho.di.trans.step.StepMetaInterface;
import org.pentaho.di.trans.step.errorhandling.Stream;
import org.pentaho.di.trans.step.errorhandling.StreamIcon;
import org.pentaho.di.trans.step.errorhandling.StreamInterface;
import org.pentaho.di.trans.step.errorhandling.StreamInterface.StreamType;
import org.pentaho.di.trans.steps.tableinput.TableInputMeta;
import org.pentaho.di.ui.core.ConstUI;
import org.pentaho.di.ui.core.PropsUI;
import org.pentaho.di.ui.core.dialog.DialogClosedListener;
import org.pentaho.di.ui.core.dialog.EnterSelectionDialog;
import org.pentaho.di.ui.core.dialog.EnterStringDialog;
import org.pentaho.di.ui.core.dialog.EnterTextDialog;
import org.pentaho.di.ui.core.dialog.ErrorDialog;
import org.pentaho.di.ui.core.dialog.PreviewRowsDialog;
import org.pentaho.di.ui.core.dialog.StepFieldsDialog;
import org.pentaho.di.ui.core.gui.GUIResource;
import org.pentaho.di.ui.core.widget.CheckBoxToolTip;
import org.pentaho.di.ui.core.widget.CheckBoxToolTipListener;
import org.pentaho.di.ui.repository.RepositorySecurityUI;
import org.pentaho.di.ui.repository.dialog.RepositoryExplorerDialog;
import org.pentaho.di.ui.repository.dialog.RepositoryRevisionBrowserDialogInterface;
import org.pentaho.di.ui.spoon.AbstractGraph;
import org.pentaho.di.ui.spoon.SWTGC;
import org.pentaho.di.ui.spoon.Spoon;
import org.pentaho.di.ui.spoon.SpoonPluginManager;
import org.pentaho.di.ui.spoon.SpoonUiExtenderPluginInterface;
import org.pentaho.di.ui.spoon.SpoonUiExtenderPluginType;
import org.pentaho.di.ui.spoon.SwtScrollBar;
import org.pentaho.di.ui.spoon.TabItemInterface;
import org.pentaho.di.ui.spoon.XulSpoonResourceBundle;
import org.pentaho.di.ui.spoon.XulSpoonSettingsManager;
import org.pentaho.di.ui.spoon.dialog.EnterPreviewRowsDialog;
import org.pentaho.di.ui.spoon.dialog.NotePadDialog;
import org.pentaho.di.ui.spoon.dialog.SearchFieldsProgressDialog;
import org.pentaho.di.ui.spoon.job.JobGraph;
import org.pentaho.di.ui.trans.dialog.TransDialog;
import org.pentaho.di.ui.xul.KettleXulLoader;
import org.pentaho.ui.xul.XulDomContainer;
import org.pentaho.ui.xul.XulException;
import org.pentaho.ui.xul.components.XulMenuitem;
import org.pentaho.ui.xul.components.XulToolbarbutton;
import org.pentaho.ui.xul.containers.XulMenu;
import org.pentaho.ui.xul.containers.XulMenupopup;
import org.pentaho.ui.xul.containers.XulToolbar;
import org.pentaho.ui.xul.dom.Document;
import org.pentaho.ui.xul.impl.XulEventHandler;
import org.pentaho.ui.xul.jface.tags.JfaceMenuitem;
import org.pentaho.ui.xul.jface.tags.JfaceMenupopup;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.Callable;

/**
 * This class handles the display of the transformations in a graphical way using icons, arrows, etc. One transformation
 * is handled per TransGraph
 *
 * @author Matt
 * @since 17-mei-2003
 */
public class TransGraph extends AbstractGraph implements XulEventHandler, Redrawable, TabItemInterface,
  LogParentProvidedInterface, MouseListener, MouseMoveListener, MouseTrackListener, MouseWheelListener, KeyListener {
  private static Class<?> PKG = Spoon.class; // for i18n purposes, needed by Translator2!!

  private LogChannelInterface log;

  private static final int HOP_SEL_MARGIN = 9;

  private static final String XUL_FILE_TRANS_TOOLBAR = "ui/trans-toolbar.xul";

  public static final String LOAD_TAB = "loadTab";

  public static final String PREVIEW_TRANS = "previewTrans";

  public static final String START_TEXT = BaseMessages.getString( PKG, "TransLog.Button.StartTransformation" );

  public static final String PAUSE_TEXT = BaseMessages.getString( PKG, "TransLog.Button.PauseTransformation" );

  public static final String RESUME_TEXT = BaseMessages.getString( PKG, "TransLog.Button.ResumeTransformation" );

  public static final String STOP_TEXT = BaseMessages.getString( PKG, "TransLog.Button.StopTransformation" );

  public static final String TRANS_GRAPH_ENTRY_SNIFF = "trans-graph-entry-sniff";

  public static final String TRANS_GRAPH_ENTRY_AGAIN = "trans-graph-entry-align";

  private static final int TOOLTIP_HIDE_DELAY_SHORT = 5000;

  private static final int TOOLTIP_HIDE_DELAY_LONG = 10000;

  private TransMeta transMeta;

  public Trans trans;

  private Shell shell;

  private Composite mainComposite;

  private DefaultToolTip toolTip;

  private CheckBoxToolTip helpTip;

  private XulToolbar toolbar;

  private int iconsize;

  private Point lastclick;

  private Point lastMove;

  private Point[] previous_step_locations;

  private Point[] previous_note_locations;

  private List<StepMeta> selectedSteps;

  private StepMeta selectedStep;

  private List<StepMeta> mouseOverSteps;

  private List<NotePadMeta> selectedNotes;

  private NotePadMeta selectedNote;

  private TransHopMeta candidate;

  private Point drop_candidate;

  private Spoon spoon;

  // public boolean shift, control;

  private boolean split_hop;

  private int lastButton;

  private TransHopMeta last_hop_split;

  private org.pentaho.di.core.gui.Rectangle selectionRegion;

  /**
   * A list of remarks on the current Transformation...
   */
  private List<CheckResultInterface> remarks;

  /**
   * A list of impacts of the current transformation on the used databases.
   */
  private List<DatabaseImpact> impact;

  /**
   * Indicates whether or not an impact analysis has already run.
   */
  private boolean impactFinished;

  private TransDebugMeta lastTransDebugMeta;

  private Map<String, XulMenupopup> menuMap = new HashMap<>();

  protected int currentMouseX = 0;

  protected int currentMouseY = 0;

  protected NotePadMeta ni = null;

  protected TransHopMeta currentHop;

  protected StepMeta currentStep;

  private List<AreaOwner> areaOwners;

  // private Text filenameLabel;
  private SashForm sashForm;

  public Composite extraViewComposite;

  public CTabFolder extraViewTabFolder;

  private boolean initialized;

  private boolean running;

  private boolean halted;

  private boolean halting;

  private boolean safeStopping;

  private boolean debug;

  private boolean pausing;

  public TransLogDelegate transLogDelegate;

  public TransGridDelegate transGridDelegate;

  public TransHistoryDelegate transHistoryDelegate;

  public TransPerfDelegate transPerfDelegate;

  public TransMetricsDelegate transMetricsDelegate;

  public TransPreviewDelegate transPreviewDelegate;

  public List<SelectedStepListener> stepListeners;

  public List<StepSelectionListener> currentStepListeners = new ArrayList<>();

  /**
   * A map that keeps track of which log line was written by which step
   */
  private Map<StepMeta, String> stepLogMap;

  private StepMeta startHopStep;
  private Point endHopLocation;
  private boolean startErrorHopStep;

  private StepMeta noInputStep;

  private StepMeta endHopStep;

  private StreamType candidateHopType;

  private Map<StepMeta, DelayTimer> delayTimers;

  private StepMeta showTargetStreamsStep;

  Timer redrawTimer;
  private ToolItem stopItem;

  public void setCurrentNote( NotePadMeta ni ) {
    this.ni = ni;
  }

  public NotePadMeta getCurrentNote() {
    return ni;
  }

  public TransHopMeta getCurrentHop() {
    return currentHop;
  }

  public void setCurrentHop( TransHopMeta currentHop ) {
    this.currentHop = currentHop;
  }

  public StepMeta getCurrentStep() {
    return currentStep;
  }

  public void setCurrentStep( StepMeta currentStep ) {
    this.currentStep = currentStep;
  }

  public void addSelectedStepListener( SelectedStepListener selectedStepListener ) {
    stepListeners.add( selectedStepListener );
  }

  public void addCurrentStepListener( StepSelectionListener stepSelectionListener ) {
    currentStepListeners.add( stepSelectionListener );
  }

  public TransGraph( Composite parent, final Spoon spoon, final TransMeta transMeta ) {
    super( parent, SWT.NONE );
    this.shell = parent.getShell();
    this.spoon = spoon;
    this.transMeta = transMeta;
    this.areaOwners = new ArrayList<>();

    this.log = spoon.getLog();
    spoon.selectionFilter.setText( "" );

    this.mouseOverSteps = new ArrayList<>();
    this.delayTimers = new HashMap<>();

    transLogDelegate = new TransLogDelegate( spoon, this );
    transGridDelegate = new TransGridDelegate( spoon, this );
    transHistoryDelegate = new TransHistoryDelegate( spoon, this );
    transPerfDelegate = new TransPerfDelegate( spoon, this );
    transMetricsDelegate = new TransMetricsDelegate( spoon, this );
    transPreviewDelegate = new TransPreviewDelegate( spoon, this );

    stepListeners = new ArrayList<>();

    try {
      KettleXulLoader loader = new KettleXulLoader();
      loader.setIconsSize( 16, 16 );
      loader.setSettingsManager( XulSpoonSettingsManager.getInstance() );
      ResourceBundle bundle = new XulSpoonResourceBundle( Spoon.class );
      XulDomContainer container = loader.loadXul( XUL_FILE_TRANS_TOOLBAR, bundle );
      container.addEventHandler( this );

      SpoonPluginManager.getInstance().applyPluginsForContainer( "trans-graph", xulDomContainer );

      setXulDomContainer( container );
    } catch ( XulException e1 ) {
      log.logError( "Error loading XUL resource bundle for Spoon", e1 );
    }

    setLayout( new FormLayout() );
    setLayoutData( new GridData( GridData.FILL_BOTH ) );

    // Add a tool-bar at the top of the tab
    // The form-data is set on the native widget automatically
    //
    addToolBar();

    setControlStates(); // enable / disable the icons in the toolbar too.

    // The main composite contains the graph view, but if needed also
    // a view with an extra tab containing log, etc.
    //
    mainComposite = new Composite( this, SWT.NONE );
    mainComposite.setLayout( new FillLayout() );

    // Nick's fix below -------
    Control toolbarControl = (Control) toolbar.getManagedObject();

    FormData toolbarFd = new FormData();
    toolbarFd.left = new FormAttachment( 0, 0 );
    toolbarFd.right = new FormAttachment( 100, 0 );

    toolbarControl.setLayoutData( toolbarFd );
    toolbarControl.setParent( this );
    // ------------------------

    FormData fdMainComposite = new FormData();
    fdMainComposite.left = new FormAttachment( 0, 0 );
    fdMainComposite.top = new FormAttachment( (Control) toolbar.getManagedObject(), 0 );
    fdMainComposite.right = new FormAttachment( 100, 0 );
    fdMainComposite.bottom = new FormAttachment( 100, 0 );
    mainComposite.setLayoutData( fdMainComposite );

    // To allow for a splitter later on, we will add the splitter here...
    //
    sashForm = new SashForm( mainComposite, SWT.VERTICAL );

    // Add a canvas below it, use up all space initially
    //
    canvas = new Canvas( sashForm, SWT.V_SCROLL | SWT.H_SCROLL | SWT.NO_BACKGROUND | SWT.BORDER );

    sashForm.setWeights( new int[] { 100, } );

    try {
      // first get the XML document
      menuMap.put( "trans-graph-hop", (XulMenupopup) getXulDomContainer().getDocumentRoot().getElementById(
        "trans-graph-hop" ) );
      menuMap.put( "trans-graph-entry", (XulMenupopup) getXulDomContainer().getDocumentRoot().getElementById(
        "trans-graph-entry" ) );
      menuMap.put( "trans-graph-background", (XulMenupopup) getXulDomContainer().getDocumentRoot().getElementById(
        "trans-graph-background" ) );
      menuMap.put( "trans-graph-note", (XulMenupopup) getXulDomContainer().getDocumentRoot().getElementById(
        "trans-graph-note" ) );
    } catch ( Throwable t ) {
      log.logError( "Error parsing XUL XML", t );
    }

    toolTip = new DefaultToolTip( canvas, ToolTip.NO_RECREATE, true );
    toolTip.setRespectMonitorBounds( true );
    toolTip.setRespectDisplayBounds( true );
    toolTip.setPopupDelay( 350 );
    toolTip.setHideDelay( TOOLTIP_HIDE_DELAY_SHORT );
    toolTip.setShift( new org.eclipse.swt.graphics.Point( ConstUI.TOOLTIP_OFFSET, ConstUI.TOOLTIP_OFFSET ) );

    helpTip = new CheckBoxToolTip( canvas );
    helpTip.addCheckBoxToolTipListener( new CheckBoxToolTipListener() {

      @Override
      public void checkBoxSelected( boolean enabled ) {
        spoon.props.setShowingHelpToolTips( enabled );
      }
    } );

    iconsize = spoon.props.getIconSize();

    clearSettings();

    remarks = new ArrayList<>();
    impact = new ArrayList<>();
    impactFinished = false;

    hori = canvas.getHorizontalBar();
    vert = canvas.getVerticalBar();

    hori.addSelectionListener( new SelectionAdapter() {
      @Override
      public void widgetSelected( SelectionEvent e ) {
        redraw();
      }
    } );
    vert.addSelectionListener( new SelectionAdapter() {
      @Override
      public void widgetSelected( SelectionEvent e ) {
        redraw();
      }
    } );
    hori.setThumb( 100 );
    vert.setThumb( 100 );

    hori.setVisible( true );
    vert.setVisible( true );

    setVisible( true );
    newProps();

    canvas.setBackground( GUIResource.getInstance().getColorBackground() );

    canvas.addPaintListener( new PaintListener() {
      @Override
      public void paintControl( PaintEvent e ) {
        if ( !spoon.isStopped() ) {
          TransGraph.this.paintControl( e );
        }
      }
    } );

    selectedSteps = null;
    lastclick = null;

    /*
     * Handle the mouse...
     */

    canvas.addMouseListener( this );
    canvas.addMouseMoveListener( this );
    canvas.addMouseTrackListener( this );
    canvas.addMouseWheelListener( this );
    canvas.addKeyListener( this );

    // Drag & Drop for steps
    Transfer[] ttypes = new Transfer[] { XMLTransfer.getInstance() };
    DropTarget ddTarget = new DropTarget( canvas, DND.DROP_MOVE );
    ddTarget.setTransfer( ttypes );
    ddTarget.addDropListener( new DropTargetListener() {
      @Override
      public void dragEnter( DropTargetEvent event ) {
        clearSettings();

        drop_candidate = PropsUI.calculateGridPosition( getRealPosition( canvas, event.x, event.y ) );

        redraw();
      }

      @Override
      public void dragLeave( DropTargetEvent event ) {
        drop_candidate = null;
        redraw();
      }

      @Override
      public void dragOperationChanged( DropTargetEvent event ) {
      }

      @Override
      public void dragOver( DropTargetEvent event ) {
        drop_candidate = PropsUI.calculateGridPosition( getRealPosition( canvas, event.x, event.y ) );

        redraw();
      }

      @Override
      public void drop( DropTargetEvent event ) {
        // no data to copy, indicate failure in event.detail
        if ( event.data == null ) {
          event.detail = DND.DROP_NONE;
          return;
        }

        // System.out.println("Dropping a step!!");

        // What's the real drop position?
        Point p = getRealPosition( canvas, event.x, event.y );

        //
        // We expect a Drag and Drop container... (encased in XML)
        try {
          DragAndDropContainer container = (DragAndDropContainer) event.data;

          StepMeta stepMeta = null;
          boolean newstep = false;

          switch ( container.getType() ) {
            // Put an existing one on the canvas.
            case DragAndDropContainer.TYPE_STEP:
              // Drop hidden step onto canvas....
              stepMeta = transMeta.findStep( container.getData() );
              if ( stepMeta != null ) {
                if ( stepMeta.isDrawn() || transMeta.isStepUsedInTransHops( stepMeta ) ) {
                  modalMessageDialog( getString( "TransGraph.Dialog.StepIsAlreadyOnCanvas.Title" ),
                    getString( "TransGraph.Dialog.StepIsAlreadyOnCanvas.Message" ), SWT.OK );
                  return;
                }
                // This step gets the drawn attribute and position set below.
              } else {
                // Unknown step dropped: ignore this to be safe!
                return;
              }
              break;

            // Create a new step
            case DragAndDropContainer.TYPE_BASE_STEP_TYPE:
              // Not an existing step: data refers to the type of step to create
              String id = container.getId();
              String name = container.getData();
              stepMeta = spoon.newStep( transMeta, id, name, name, false, true );
              if ( stepMeta != null ) {
                newstep = true;
              } else {
                return; // Cancelled pressed in dialog or unable to create step.
              }
              break;

            // Create a new TableInput step using the selected connection...
            case DragAndDropContainer.TYPE_DATABASE_CONNECTION:
              newstep = true;
              String connectionName = container.getData();
              TableInputMeta tii = new TableInputMeta();
              tii.setDatabaseMeta( transMeta.findDatabase( connectionName ) );
              PluginRegistry registry = PluginRegistry.getInstance();
              String stepID = registry.getPluginId( StepPluginType.class, tii );
              PluginInterface stepPlugin = registry.findPluginWithId( StepPluginType.class, stepID );
              String stepName = transMeta.getAlternativeStepname( stepPlugin.getName() );
              stepMeta = new StepMeta( stepID, stepName, tii );
              if ( spoon.editStep( transMeta, stepMeta ) != null ) {
                transMeta.addStep( stepMeta );
                spoon.refreshTree();
                spoon.refreshGraph();
              } else {
                return;
              }
              break;

            // Drag hop on the canvas: create a new Hop...
            case DragAndDropContainer.TYPE_TRANS_HOP:
              newHop();
              return;

            default:
              // Nothing we can use: give an error!
              modalMessageDialog( getString( "TransGraph.Dialog.ItemCanNotBePlacedOnCanvas.Title" ),
                getString( "TransGraph.Dialog.ItemCanNotBePlacedOnCanvas.Message" ), SWT.OK );
              return;
          }

          transMeta.unselectAll();

          StepMeta before = null;
          if ( !newstep ) {
            before = (StepMeta) stepMeta.clone();
          }

          stepMeta.drawStep();
          stepMeta.setSelected( true );
          PropsUI.setLocation( stepMeta, p.x, p.y );

          if ( newstep ) {
            spoon.addUndoNew( transMeta, new StepMeta[] { stepMeta }, new int[] { transMeta.indexOfStep( stepMeta ) } );
          } else {
            spoon.addUndoChange( transMeta, new StepMeta[] { before }, new StepMeta[] { (StepMeta) stepMeta.clone() },
              new int[] { transMeta.indexOfStep( stepMeta ) } );
          }

          canvas.forceFocus();
          redraw();

          // See if we want to draw a tool tip explaining how to create new hops...
          //
          if ( newstep && transMeta.nrSteps() > 1 && transMeta.nrSteps() < 5 && spoon.props.isShowingHelpToolTips() ) {
            showHelpTip( p.x, p.y, BaseMessages.getString( PKG, "TransGraph.HelpToolTip.CreatingHops.Title" ),
              BaseMessages.getString( PKG, "TransGraph.HelpToolTip.CreatingHops.Message" ) );
          }
        } catch ( Exception e ) {
          new ErrorDialog( shell, BaseMessages.getString( PKG, "TransGraph.Dialog.ErrorDroppingObject.Message" ),
            BaseMessages.getString( PKG, "TransGraph.Dialog.ErrorDroppingObject.Title" ), e );
        }
      }

      @Override
      public void dropAccept( DropTargetEvent event ) {
      }
    } );

    setBackground( GUIResource.getInstance().getColorBackground() );

    // Add a timer to set correct the state of the run/stop buttons every 2 seconds...
    //
    final Timer timer = new Timer( "TransGraph.setControlStates Timer: " + getMeta().getName() );
    TimerTask timerTask = new TimerTask() {

      @Override
      public void run() {
        try {
          setControlStates();
        } catch ( KettleRepositoryLostException krle ) {
          if ( log.isBasic() ) {
            log.logBasic( krle.getLocalizedMessage() );
          }
        }
      }
    };
    timer.schedule( timerTask, 2000, 1000 );

    // Make sure the timer stops when we close the tab...
    //
    addDisposeListener( new DisposeListener() {
      @Override
      public void widgetDisposed( DisposeEvent arg0 ) {
        timer.cancel();
      }
    } );
  }

  @Override
  public void mouseDoubleClick( MouseEvent e ) {
    clearSettings();

    Point real = screen2real( e.x, e.y );

    // Hide the tooltip!
    hideToolTips();

    try {
      ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransGraphMouseDoubleClick.id,
        new TransGraphExtension( this, e, real ) );
    } catch ( Exception ex ) {
      LogChannel.GENERAL.logError( "Error calling TransGraphMouseDoubleClick extension point", ex );
    }

    StepMeta stepMeta = transMeta.getStep( real.x, real.y, iconsize );
    if ( stepMeta != null ) {
      if ( e.button == 1 ) {
        editStep( stepMeta );
      } else {
        editDescription( stepMeta );
      }
    } else {
      // Check if point lies on one of the many hop-lines...
      TransHopMeta online = findHop( real.x, real.y );
      if ( online != null ) {
        editHop( online );
      } else {
        NotePadMeta ni = transMeta.getNote( real.x, real.y );
        if ( ni != null ) {
          selectedNote = null;
          editNote( ni );
        } else {
          // See if the double click was in one of the area's...
          //
          boolean hit = false;
          for ( AreaOwner areaOwner : areaOwners ) {
            if ( areaOwner.contains( real.x, real.y ) ) {
              if ( areaOwner.getParent() instanceof StepMeta
                && areaOwner.getOwner().equals( TransPainter.STRING_PARTITIONING_CURRENT_STEP ) ) {
                StepMeta step = (StepMeta) areaOwner.getParent();
                spoon.editPartitioning( transMeta, step );
                hit = true;
                break;
              }
            }
          }

          if ( !hit ) {
            settings();
          }

        }
      }
    }
  }

  @Override
  public void mouseDown( MouseEvent e ) {

    boolean alt = ( e.stateMask & SWT.ALT ) != 0;
    boolean control = ( e.stateMask & SWT.MOD1 ) != 0;
    boolean shift = ( e.stateMask & SWT.SHIFT ) != 0;

    lastButton = e.button;
    Point real = screen2real( e.x, e.y );
    lastclick = new Point( real.x, real.y );

    // Hide the tooltip!
    hideToolTips();

    // Set the pop-up menu
    if ( e.button == 3 ) {
      setMenu( real.x, real.y );
      return;
    }

    try {
      ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransGraphMouseDown.id,
        new TransGraphExtension( this, e, real ) );
    } catch ( Exception ex ) {
      LogChannel.GENERAL.logError( "Error calling TransGraphMouseDown extension point", ex );
    }

    // A single left or middle click on one of the area owners...
    //
    if ( e.button == 1 || e.button == 2 ) {
      AreaOwner areaOwner = getVisibleAreaOwner( real.x, real.y );
      if ( areaOwner != null && areaOwner.getAreaType() != null ) {
        switch ( areaOwner.getAreaType() ) {
          case STEP_OUTPUT_HOP_ICON:
            // Click on the output icon means: start of drag
            // Action: We show the input icons on the other steps...
            //
            selectedStep = null;
            startHopStep = (StepMeta) areaOwner.getParent();
            candidateHopType = null;
            startErrorHopStep = false;
            // stopStepMouseOverDelayTimer(startHopStep);
            break;

          case STEP_INPUT_HOP_ICON:
            // Click on the input icon means: start to a new hop
            // In this case, we set the end hop step...
            //
            selectedStep = null;
            startHopStep = null;
            endHopStep = (StepMeta) areaOwner.getParent();
            candidateHopType = null;
            startErrorHopStep = false;
            // stopStepMouseOverDelayTimer(endHopStep);
            break;

          case HOP_ERROR_ICON:
            // Click on the error icon means: Edit error handling
            //
            StepMeta stepMeta = (StepMeta) areaOwner.getParent();
            spoon.editStepErrorHandling( transMeta, stepMeta );
            break;

          case STEP_TARGET_HOP_ICON_OPTION:
            // Below, see showStepTargetOptions()
            break;

          case STEP_EDIT_ICON:
            clearSettings();
            currentStep = (StepMeta) areaOwner.getParent();
            stopStepMouseOverDelayTimer( currentStep );
            editStep();
            break;

          case STEP_INJECT_ICON:
            modalMessageDialog( getString( "TransGraph.StepInjectionSupported.Title" ),
              getString( "TransGraph.StepInjectionSupported.Tooltip" ), SWT.OK | SWT.ICON_INFORMATION );
            break;

          case STEP_MENU_ICON:
            clearSettings();
            stepMeta = (StepMeta) areaOwner.getParent();
            setMenu( stepMeta.getLocation().x, stepMeta.getLocation().y );
            break;

          case STEP_ICON:
            stepMeta = (StepMeta) areaOwner.getOwner();
            currentStep = stepMeta;

            for ( StepSelectionListener listener : currentStepListeners ) {
              listener.onUpdateSelection( currentStep );
            }

            if ( candidate != null ) {
              addCandidateAsHop( e.x, e.y );
            } else {
              if ( transPreviewDelegate.isActive() ) {
                transPreviewDelegate.setSelectedStep( currentStep );

                for ( SelectedStepListener stepListener : stepListeners ) {
                  if ( this.extraViewComposite != null && !this.extraViewComposite.isDisposed() ) {
                    stepListener.onSelect( currentStep );
                  }
                }

                transPreviewDelegate.refreshView();
              }
            }
            // ALT-Click: edit error handling
            //
            if ( e.button == 1 && alt && stepMeta.supportsErrorHandling() ) {
              spoon.editStepErrorHandling( transMeta, stepMeta );
              return;
            } else if ( e.button == 2 || ( e.button == 1 && shift ) ) {
              // SHIFT CLICK is start of drag to create a new hop
              //
              startHopStep = stepMeta;
            } else {
              selectedSteps = transMeta.getSelectedSteps();
              selectedStep = stepMeta;
              //
              // When an icon is moved that is not selected, it gets
              // selected too late.
              // It is not captured here, but in the mouseMoveListener...
              //
              previous_step_locations = transMeta.getSelectedStepLocations();

              Point p = stepMeta.getLocation();
              iconoffset = new Point( real.x - p.x, real.y - p.y );
            }
            redraw();
            break;

          case NOTE:
            ni = (NotePadMeta) areaOwner.getOwner();
            selectedNotes = transMeta.getSelectedNotes();
            selectedNote = ni;
            Point loc = ni.getLocation();

            previous_note_locations = transMeta.getSelectedNoteLocations();

            noteoffset = new Point( real.x - loc.x, real.y - loc.y );

            redraw();
            break;

          case STEP_COPIES_TEXT:
            copies( (StepMeta) areaOwner.getOwner() );
            break;

          case STEP_DATA_SERVICE:
            editProperties( transMeta, spoon, spoon.getRepository(), true, TransDialog.Tabs.EXTRA_TAB );
            break;
          default:
            break;
        }
      } else {
        // A hop? --> enable/disable
        //
        TransHopMeta hop = findHop( real.x, real.y );
        if ( hop != null ) {
          TransHopMeta before = (TransHopMeta) hop.clone();
          setHopEnabled( hop, !hop.isEnabled() );
          if ( hop.isEnabled() && transMeta.hasLoop( hop.getToStep() ) ) {
            setHopEnabled( hop, false );
            modalMessageDialog( getString( "TransGraph.Dialog.HopCausesLoop.Title" ),
              getString( "TransGraph.Dialog.HopCausesLoop.Message" ), SWT.OK | SWT.ICON_ERROR );
          }
          TransHopMeta after = (TransHopMeta) hop.clone();
          spoon.addUndoChange( transMeta, new TransHopMeta[] { before }, new TransHopMeta[] { after },
            new int[] { transMeta.indexOfTransHop( hop ) } );
          redraw();
          spoon.setShellText();
        } else {
          // No area-owner & no hop means : background click:
          //
          startHopStep = null;
          if ( !control ) {
            selectionRegion = new org.pentaho.di.core.gui.Rectangle( real.x, real.y, 0, 0 );
          }
          redraw();
        }
      }
    }
  }

  @Override
  public void mouseUp( MouseEvent e ) {
    try {
      TransGraphExtension ext = new TransGraphExtension( null, e, getArea() );
      ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransGraphMouseUp.id, ext );
      if ( ext.isPreventDefault() ) {
        redraw();
        clearSettings();
        return;
      }
    } catch ( Exception ex ) {
      LogChannel.GENERAL.logError( "Error calling TransGraphMouseUp extension point", ex );
    }

    boolean control = ( e.stateMask & SWT.MOD1 ) != 0;
    TransHopMeta selectedHop = findHop( e.x, e.y );
    updateErrorMetaForHop( selectedHop );

    if ( iconoffset == null ) {
      iconoffset = new Point( 0, 0 );
    }
    Point real = screen2real( e.x, e.y );
    Point icon = new Point( real.x - iconoffset.x, real.y - iconoffset.y );
    AreaOwner areaOwner = getVisibleAreaOwner( real.x, real.y );

    try {
      TransGraphExtension ext = new TransGraphExtension( this, e, real );
      ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransGraphMouseUp.id, ext );
      if ( ext.isPreventDefault() ) {
        redraw();
        clearSettings();
        return;
      }
    } catch ( Exception ex ) {
      LogChannel.GENERAL.logError( "Error calling TransGraphMouseUp extension point", ex );
    }

    // Quick new hop option? (drag from one step to another)
    //
    if ( candidate != null && areaOwner != null && areaOwner.getAreaType() != null ) {
      switch ( areaOwner.getAreaType() ) {
        case STEP_ICON:
          currentStep = (StepMeta) areaOwner.getOwner();
          break;
        case STEP_INPUT_HOP_ICON:
          currentStep = (StepMeta) areaOwner.getParent();
          break;
        default:
          break;
      }
      addCandidateAsHop( e.x, e.y );
      redraw();
    } else {
      // Did we select a region on the screen? Mark steps in region as
      // selected
      //
      if ( selectionRegion != null ) {
        selectionRegion.width = real.x - selectionRegion.x;
        selectionRegion.height = real.y - selectionRegion.y;

        transMeta.unselectAll();
        selectInRect( transMeta, selectionRegion );
        selectionRegion = null;
        stopStepMouseOverDelayTimers();
        redraw();
      } else {
        // Clicked on an icon?
        //
        if ( selectedStep != null && startHopStep == null ) {
          if ( e.button == 1 ) {
            Point realclick = screen2real( e.x, e.y );
            if ( lastclick.x == realclick.x && lastclick.y == realclick.y ) {
              // Flip selection when control is pressed!
              if ( control ) {
                selectedStep.flipSelected();
              } else {
                // Otherwise, select only the icon clicked on!
                transMeta.unselectAll();
                selectedStep.setSelected( true );
              }
            } else {
              // Find out which Steps & Notes are selected
              selectedSteps = transMeta.getSelectedSteps();
              selectedNotes = transMeta.getSelectedNotes();

              // We moved around some items: store undo info...
              //
              boolean also = false;
              if ( selectedNotes != null && selectedNotes.size() > 0 && previous_note_locations != null ) {
                int[] indexes = transMeta.getNoteIndexes( selectedNotes );
                addUndoPosition( selectedNotes.toArray( new NotePadMeta[ selectedNotes.size() ] ), indexes,
                  previous_note_locations, transMeta.getSelectedNoteLocations(), also );
                also = selectedSteps != null && selectedSteps.size() > 0;
              }
              if ( selectedSteps != null && previous_step_locations != null ) {
                int[] indexes = transMeta.getStepIndexes( selectedSteps );
                addUndoPosition( selectedSteps.toArray( new StepMeta[ selectedSteps.size() ] ), indexes,
                  previous_step_locations, transMeta.getSelectedStepLocations(), also );
              }
            }
          }

          // OK, we moved the step, did we move it across a hop?
          // If so, ask to split the hop!
          if ( split_hop ) {
            TransHopMeta hi = findHop( icon.x + iconsize / 2, icon.y + iconsize / 2, selectedStep );
            if ( hi != null ) {
              splitHop( hi );
            }
            split_hop = false;
          }

          selectedSteps = null;
          selectedNotes = null;
          selectedStep = null;
          selectedNote = null;
          startHopStep = null;
          endHopLocation = null;
          redraw();
          spoon.setShellText();
        } else {
          // Notes?
          //

          if ( selectedNote != null ) {
            if ( e.button == 1 ) {
              if ( lastclick.x == e.x && lastclick.y == e.y ) {
                // Flip selection when control is pressed!
                if ( control ) {
                  selectedNote.flipSelected();
                } else {
                  // Otherwise, select only the note clicked on!
                  transMeta.unselectAll();
                  selectedNote.setSelected( true );
                }
              } else {
                // Find out which Steps & Notes are selected
                selectedSteps = transMeta.getSelectedSteps();
                selectedNotes = transMeta.getSelectedNotes();

                // We moved around some items: store undo info...
                boolean also = false;
                if ( selectedNotes != null && selectedNotes.size() > 0 && previous_note_locations != null ) {
                  int[] indexes = transMeta.getNoteIndexes( selectedNotes );
                  addUndoPosition( selectedNotes.toArray( new NotePadMeta[ selectedNotes.size() ] ), indexes,
                    previous_note_locations, transMeta.getSelectedNoteLocations(), also );
                  also = selectedSteps != null && selectedSteps.size() > 0;
                }
                if ( selectedSteps != null && selectedSteps.size() > 0 && previous_step_locations != null ) {
                  int[] indexes = transMeta.getStepIndexes( selectedSteps );
                  addUndoPosition( selectedSteps.toArray( new StepMeta[ selectedSteps.size() ] ), indexes,
                    previous_step_locations, transMeta.getSelectedStepLocations(), also );
                }
              }
            }

            selectedNotes = null;
            selectedSteps = null;
            selectedStep = null;
            selectedNote = null;
            startHopStep = null;
            endHopLocation = null;
          } else {
            if ( areaOwner == null && selectionRegion == null ) {
              // Hit absolutely nothing: clear the settings
              //
              clearSettings();
            }
          }
        }
      }
    }

    lastButton = 0;
  }

  private void splitHop( TransHopMeta hi ) {
    int id = 0;
    if ( !spoon.props.getAutoSplit() ) {
      MessageDialogWithToggle md =
        new MessageDialogWithToggle( shell, BaseMessages.getString( PKG, "TransGraph.Dialog.SplitHop.Title" ), null,
          BaseMessages.getString( PKG, "TransGraph.Dialog.SplitHop.Message" ) + Const.CR + hi.toString(),
          MessageDialog.QUESTION, new String[] { BaseMessages.getString( PKG, "System.Button.Yes" ),
          BaseMessages.getString( PKG, "System.Button.No" ) }, 0, BaseMessages.getString( PKG,
          "TransGraph.Dialog.Option.SplitHop.DoNotAskAgain" ), spoon.props.getAutoSplit() );
      MessageDialogWithToggle.setDefaultImage( GUIResource.getInstance().getImageSpoon() );
      id = md.open();
      spoon.props.setAutoSplit( md.getToggleState() );
    }

    if ( ( id & 0xFF ) == 0 ) { // Means: "Yes" button clicked!

      // Only split A-->--B by putting C in between IF...
      // C-->--A or B-->--C don't exists...
      // A ==> hi.getFromStep()
      // B ==> hi.getToStep();
      // C ==> selected_step
      //
      boolean caExists = transMeta.findTransHop( selectedStep, hi.getFromStep() ) != null;
      boolean bcExists = transMeta.findTransHop( hi.getToStep(), selectedStep ) != null;
      if ( !caExists && !bcExists ) {

        StepMeta fromStep = hi.getFromStep();
        StepMeta toStep = hi.getToStep();

        // In case step A targets B then we now need to target C
        //
        StepIOMetaInterface fromIo = fromStep.getStepMetaInterface().getStepIOMeta();
        for ( StreamInterface stream : fromIo.getTargetStreams() ) {
          if ( stream.getStepMeta() != null && stream.getStepMeta().equals( toStep ) ) {
            // This target stream was directed to B, now we need to direct it to C
            stream.setStepMeta( selectedStep );
            fromStep.getStepMetaInterface().handleStreamSelection( stream );
          }
        }

        // In case step B sources from A then we now need to source from C
        //
        StepIOMetaInterface toIo = toStep.getStepMetaInterface().getStepIOMeta();
        for ( StreamInterface stream : toIo.getInfoStreams() ) {
          if ( stream.getStepMeta() != null && stream.getStepMeta().equals( fromStep ) ) {
            // This info stream was reading from B, now we need to direct it to C
            stream.setStepMeta( selectedStep );
            toStep.getStepMetaInterface().handleStreamSelection( stream );
          }
        }

        // In case there is error handling on A, we want to make it point to C now
        //
        StepErrorMeta errorMeta = fromStep.getStepErrorMeta();
        if ( fromStep.isDoingErrorHandling() && toStep.equals( errorMeta.getTargetStep() ) ) {
          errorMeta.setTargetStep( selectedStep );
        }

        TransHopMeta newhop1 = new TransHopMeta( hi.getFromStep(), selectedStep );
        if ( transMeta.findTransHop( newhop1 ) == null ) {
          transMeta.addTransHop( newhop1 );
          spoon.addUndoNew( transMeta, new TransHopMeta[] { newhop1, },
            new int[] { transMeta.indexOfTransHop( newhop1 ), }, true );
        }
        TransHopMeta newhop2 = new TransHopMeta( selectedStep, hi.getToStep() );
        if ( transMeta.findTransHop( newhop2 ) == null ) {
          transMeta.addTransHop( newhop2 );
          spoon.addUndoNew( transMeta, new TransHopMeta[] { newhop2 },
            new int[] { transMeta.indexOfTransHop( newhop2 ) }, true );
        }
        int idx = transMeta.indexOfTransHop( hi );
        spoon.addUndoDelete( transMeta, new TransHopMeta[] { hi }, new int[] { idx }, true );
        transMeta.removeTransHop( idx );
        spoon.refreshTree();

      }

      // else: Silently discard this hop-split attempt.
    }
  }

  @Override
  public void mouseMove( MouseEvent e ) {
    boolean shift = ( e.stateMask & SWT.SHIFT ) != 0;
    noInputStep = null;

    // disable the tooltip
    //
    toolTip.hide();
    toolTip.setHideDelay( TOOLTIP_HIDE_DELAY_SHORT );

    Point real = screen2real( e.x, e.y );
    // Remember the last position of the mouse for paste with keyboard
    //
    lastMove = real;

    if ( iconoffset == null ) {
      iconoffset = new Point( 0, 0 );
    }
    Point icon = new Point( real.x - iconoffset.x, real.y - iconoffset.y );

    if ( noteoffset == null ) {
      noteoffset = new Point( 0, 0 );
    }
    Point note = new Point( real.x - noteoffset.x, real.y - noteoffset.y );

    // Moved over an area?
    //
    AreaOwner areaOwner = getVisibleAreaOwner( real.x, real.y );
    if ( areaOwner != null && areaOwner.getAreaType() != null ) {
      switch ( areaOwner.getAreaType() ) {
        case STEP_ICON:
          StepMeta stepMeta = (StepMeta) areaOwner.getOwner();
          resetDelayTimer( stepMeta );
          break;

        case MINI_ICONS_BALLOON: // Give the timer a bit more time
          stepMeta = (StepMeta) areaOwner.getParent();
          resetDelayTimer( stepMeta );
          break;

        default:
          break;
      }
    }

    try {
      TransGraphExtension ext = new TransGraphExtension( this, e, real );
      ExtensionPointHandler.callExtensionPoint(
        LogChannel.GENERAL, KettleExtensionPoint.TransGraphMouseMoved.id, ext );

    } catch ( Exception ex ) {
      LogChannel.GENERAL.logError( "Error calling TransGraphMouseMoved extension point", ex );
    }

    //
    // First see if the icon we clicked on was selected.
    // If the icon was not selected, we should un-select all other
    // icons, selected and move only the one icon
    //
    if ( selectedStep != null && !selectedStep.isSelected() ) {
      transMeta.unselectAll();
      selectedStep.setSelected( true );
      selectedSteps = new ArrayList<>();
      selectedSteps.add( selectedStep );
      previous_step_locations = new Point[] { selectedStep.getLocation() };
      redraw();
    } else if ( selectedNote != null && !selectedNote.isSelected() ) {
      transMeta.unselectAll();
      selectedNote.setSelected( true );
      selectedNotes = new ArrayList<>();
      selectedNotes.add( selectedNote );
      previous_note_locations = new Point[] { selectedNote.getLocation() };
      redraw();
    } else if ( selectionRegion != null && startHopStep == null ) {
      // Did we select a region...?
      //

      selectionRegion.width = real.x - selectionRegion.x;
      selectionRegion.height = real.y - selectionRegion.y;
      redraw();
    } else if ( selectedStep != null && lastButton == 1 && !shift && startHopStep == null ) {
      //
      // One or more icons are selected and moved around...
      //
      // new : new position of the ICON (not the mouse pointer) dx : difference with previous position
      //
      int dx = icon.x - selectedStep.getLocation().x;
      int dy = icon.y - selectedStep.getLocation().y;

      // See if we have a hop-split candidate
      //
      TransHopMeta hi = findHop( icon.x + iconsize / 2, icon.y + iconsize / 2, selectedStep );
      if ( hi != null ) {
        // OK, we want to split the hop in 2
        //
        if ( !hi.getFromStep().equals( selectedStep ) && !hi.getToStep().equals( selectedStep ) ) {
          split_hop = true;
          last_hop_split = hi;
          hi.split = true;
        }
      } else {
        if ( last_hop_split != null ) {
          last_hop_split.split = false;
          last_hop_split = null;
          split_hop = false;
        }
      }

      selectedNotes = transMeta.getSelectedNotes();
      selectedSteps = transMeta.getSelectedSteps();

      // Adjust location of selected steps...
      if ( selectedSteps != null ) {
        for ( int i = 0; i < selectedSteps.size(); i++ ) {
          StepMeta stepMeta = selectedSteps.get( i );
          PropsUI.setLocation( stepMeta, stepMeta.getLocation().x + dx, stepMeta.getLocation().y + dy );
          stopStepMouseOverDelayTimer( stepMeta );
        }
      }
      // Adjust location of selected hops...
      if ( selectedNotes != null ) {
        for ( int i = 0; i < selectedNotes.size(); i++ ) {
          NotePadMeta ni = selectedNotes.get( i );
          PropsUI.setLocation( ni, ni.getLocation().x + dx, ni.getLocation().y + dy );
        }
      }

      redraw();
    } else if ( ( startHopStep != null && endHopStep == null ) || ( endHopStep != null && startHopStep == null ) ) {
      // Are we creating a new hop with the middle button or pressing SHIFT?
      //

      StepMeta stepMeta = transMeta.getStep( real.x, real.y, iconsize );
      endHopLocation = new Point( real.x, real.y );
      if ( stepMeta != null
        && ( ( startHopStep != null && !startHopStep.equals( stepMeta ) ) || ( endHopStep != null && !endHopStep
        .equals( stepMeta ) ) ) ) {
        StepIOMetaInterface ioMeta = stepMeta.getStepMetaInterface().getStepIOMeta();
        if ( candidate == null ) {
          // See if the step accepts input. If not, we can't create a new hop...
          //
          if ( startHopStep != null ) {
            if ( ioMeta.isInputAcceptor() ) {
              candidate = new TransHopMeta( startHopStep, stepMeta );
              endHopLocation = null;
            } else {
              noInputStep = stepMeta;
              toolTip.setImage( null );
              toolTip.setText( "This step does not accept any input from other steps" );
              toolTip.show( new org.eclipse.swt.graphics.Point( real.x, real.y ) );
            }
          } else if ( endHopStep != null ) {
            if ( ioMeta.isOutputProducer() ) {
              candidate = new TransHopMeta( stepMeta, endHopStep );
              endHopLocation = null;
            } else {
              noInputStep = stepMeta;
              toolTip.setImage( null );
              toolTip
                .setText( "This step doesn't pass any output to other steps. (except perhaps for targetted output)" );
              toolTip.show( new org.eclipse.swt.graphics.Point( real.x, real.y ) );
            }
          }
        }
      } else {
        if ( candidate != null ) {
          candidate = null;
          redraw();
        }
      }

      redraw();
    }

    // Move around notes & steps
    //
    if ( selectedNote != null ) {
      if ( lastButton == 1 && !shift ) {
        /*
         * One or more notes are selected and moved around...
         *
         * new : new position of the note (not the mouse pointer) dx : difference with previous position
         */
        int dx = note.x - selectedNote.getLocation().x;
        int dy = note.y - selectedNote.getLocation().y;

        selectedNotes = transMeta.getSelectedNotes();
        selectedSteps = transMeta.getSelectedSteps();

        // Adjust location of selected steps...
        if ( selectedSteps != null ) {
          for ( int i = 0; i < selectedSteps.size(); i++ ) {
            StepMeta stepMeta = selectedSteps.get( i );
            PropsUI.setLocation( stepMeta, stepMeta.getLocation().x + dx, stepMeta.getLocation().y + dy );
          }
        }
        // Adjust location of selected hops...
        if ( selectedNotes != null ) {
          for ( int i = 0; i < selectedNotes.size(); i++ ) {
            NotePadMeta ni = selectedNotes.get( i );
            PropsUI.setLocation( ni, ni.getLocation().x + dx, ni.getLocation().y + dy );
          }
        }

        redraw();
      }
    }
  }

  @Override
  public void mouseHover( MouseEvent e ) {

    boolean tip = true;
    boolean isDeprecated = false;

    toolTip.hide();
    toolTip.setHideDelay( TOOLTIP_HIDE_DELAY_SHORT );
    Point real = screen2real( e.x, e.y );

    AreaOwner areaOwner = getVisibleAreaOwner( real.x, real.y );
    if ( areaOwner != null && areaOwner.getAreaType() != null ) {
      switch ( areaOwner.getAreaType() ) {
        case STEP_ICON:
          StepMeta stepMeta = (StepMeta) areaOwner.getOwner();
          isDeprecated = stepMeta.isDeprecated();
          if ( !stepMeta.isMissing() && !mouseOverSteps.contains( stepMeta ) ) {
            addStepMouseOverDelayTimer( stepMeta );
            redraw();
            tip = false;
          }
          break;
        default:
          break;
      }
    }

    // Show a tool tip upon mouse-over of an object on the canvas
    if ( ( tip && !helpTip.isVisible() ) || isDeprecated ) {
      setToolTip( real.x, real.y, e.x, e.y );
    }
  }

  @Override
  public void mouseScrolled( MouseEvent e ) {
    /*
     * if (e.count == 3) { // scroll up zoomIn(); } else if (e.count == -3) { // scroll down zoomOut(); } }
     */
  }

  private void addCandidateAsHop( int mouseX, int mouseY ) {

    boolean forward = startHopStep != null;

    StepMeta fromStep = candidate.getFromStep();
    StepMeta toStep = candidate.getToStep();

    // See what the options are.
    // - Does the source step has multiple stream options?
    // - Does the target step have multiple input stream options?
    //
    List<StreamInterface> streams = new ArrayList<>();

    StepIOMetaInterface fromIoMeta = fromStep.getStepMetaInterface().getStepIOMeta();
    List<StreamInterface> targetStreams = fromIoMeta.getTargetStreams();
    if ( forward ) {
      streams.addAll( targetStreams );
    }

    StepIOMetaInterface toIoMeta = toStep.getStepMetaInterface().getStepIOMeta();
    List<StreamInterface> infoStreams = toIoMeta.getInfoStreams();
    if ( !forward ) {
      streams.addAll( infoStreams );
    }

    if ( forward ) {
      if ( fromIoMeta.isOutputProducer() && toStep.equals( currentStep ) ) {
        streams.add( new Stream( StreamType.OUTPUT, fromStep, BaseMessages
          .getString( PKG, "Spoon.Hop.MainOutputOfStep" ), StreamIcon.OUTPUT, null ) );
      }

      if ( fromStep.supportsErrorHandling() && toStep.equals( currentStep ) ) {
        streams.add( new Stream( StreamType.ERROR, fromStep, BaseMessages.getString( PKG,
          "Spoon.Hop.ErrorHandlingOfStep" ), StreamIcon.ERROR, null ) );
      }
    } else {
      if ( toIoMeta.isInputAcceptor() && fromStep.equals( currentStep ) ) {
        streams.add( new Stream( StreamType.INPUT, toStep, BaseMessages.getString( PKG, "Spoon.Hop.MainInputOfStep" ),
          StreamIcon.INPUT, null ) );
      }

      if ( fromStep.supportsErrorHandling() && fromStep.equals( currentStep ) ) {
        streams.add( new Stream( StreamType.ERROR, fromStep, BaseMessages.getString( PKG,
          "Spoon.Hop.ErrorHandlingOfStep" ), StreamIcon.ERROR, null ) );
      }
    }

    // Targets can be dynamically added to this step...
    //
    if ( forward ) {
      streams.addAll( fromStep.getStepMetaInterface().getOptionalStreams() );
    } else {
      streams.addAll( toStep.getStepMetaInterface().getOptionalStreams() );
    }

    // Show a list of options on the canvas...
    //
    if ( streams.size() > 1 ) {
      // Show a pop-up menu with all the possible options...
      //
      Menu menu = new Menu( canvas );
      for ( final StreamInterface stream : streams ) {
        MenuItem item = new MenuItem( menu, SWT.NONE );
        item.setText( Const.NVL( stream.getDescription(), "" ) );
        item.setImage( getImageFor( stream ) );
        item.addSelectionListener( new SelectionAdapter() {
          @Override
          public void widgetSelected( SelectionEvent e ) {
            addHop( stream );
          }
        } );
      }
      menu.setLocation( canvas.toDisplay( mouseX, mouseY ) );
      menu.setVisible( true );

      return;
    }
    if ( streams.size() == 1 ) {
      addHop( streams.get( 0 ) );
    } else {
      return;
    }

    /*
     *
     * if (transMeta.findTransHop(candidate) == null) { spoon.newHop(transMeta, candidate); } if (startErrorHopStep) {
     * addErrorHop(); } if (startTargetHopStream != null) { // Auto-configure the target in the source step... //
     * startTargetHopStream.setStepMeta(candidate.getToStep());
     * startTargetHopStream.setStepname(candidate.getToStep().getName()); startTargetHopStream = null; }
     */
    candidate = null;
    selectedSteps = null;
    startHopStep = null;
    endHopLocation = null;
    startErrorHopStep = false;

    // redraw();
  }

  private Image getImageFor( StreamInterface stream ) {
    Display disp = shell.getDisplay();
    SwtUniversalImage swtImage = SWTGC.getNativeImage( BasePainter.getStreamIconImage( stream.getStreamIcon() ) );
    return swtImage.getAsBitmapForSize( disp, ConstUI.SMALL_ICON_SIZE, ConstUI.SMALL_ICON_SIZE );
  }

  protected void addHop( StreamInterface stream ) {
    switch ( stream.getStreamType() ) {
      case ERROR:
        addErrorHop();
        candidate.setErrorHop( true );
        spoon.newHop( transMeta, candidate );
        break;
      case INPUT:
        spoon.newHop( transMeta, candidate );
        break;
      case OUTPUT:
        StepErrorMeta stepErrorMeta = candidate.getFromStep().getStepErrorMeta();
        if ( stepErrorMeta != null && stepErrorMeta.getTargetStep() != null ) {
          if ( stepErrorMeta.getTargetStep().equals( candidate.getToStep() ) ) {
            candidate.getFromStep().setStepErrorMeta( null );
          }
        }
        spoon.newHop( transMeta, candidate );
        break;
      case INFO:
        stream.setStepMeta( candidate.getFromStep() );
        candidate.getToStep().getStepMetaInterface().handleStreamSelection( stream );
        spoon.newHop( transMeta, candidate );
        break;
      case TARGET:
        // We connect a target of the source step to an output step...
        //
        stream.setStepMeta( candidate.getToStep() );
        candidate.getFromStep().getStepMetaInterface().handleStreamSelection( stream );
        spoon.newHop( transMeta, candidate );
        break;
      default:
        break;

    }
    clearSettings();
  }

  private void addErrorHop() {
    // Automatically configure the step error handling too!
    //
    if ( candidate == null || candidate.getFromStep() == null ) {
      return;
    }
    StepErrorMeta errorMeta = candidate.getFromStep().getStepErrorMeta();
    if ( errorMeta == null ) {
      errorMeta = new StepErrorMeta( transMeta, candidate.getFromStep() );
    }
    errorMeta.setEnabled( true );
    errorMeta.setTargetStep( candidate.getToStep() );
    candidate.getFromStep().setStepErrorMeta( errorMeta );
  }

  private void resetDelayTimer( StepMeta stepMeta ) {
    DelayTimer delayTimer = delayTimers.get( stepMeta );
    if ( delayTimer != null ) {
      delayTimer.reset();
    }
  }

  /*
   * private void showStepTargetOptions(final StepMeta stepMeta, StepIOMetaInterface ioMeta, int x, int y) {
   *
   * if (!Utils.isEmpty(ioMeta.getTargetStepnames())) { final Menu menu = new Menu(canvas); for (final StreamInterface
   * stream : ioMeta.getTargetStreams()) { MenuItem menuItem = new MenuItem(menu, SWT.NONE);
   * menuItem.setText(stream.getDescription()); menuItem.addSelectionListener(new SelectionAdapter() {
   *
   * @Override public void widgetSelected(SelectionEvent arg0) { // Click on the target icon means: create a new target
   * hop // if (startHopStep==null) { startHopStep = stepMeta; } menu.setVisible(false); menu.dispose(); redraw(); } });
   * } menu.setLocation(x, y); menu.setVisible(true); resetDelayTimer(stepMeta);
   *
   * //showTargetStreamsStep = stepMeta; } }
   */

  @Override
  public void mouseEnter( MouseEvent arg0 ) {
  }

  @Override
  public void mouseExit( MouseEvent arg0 ) {
  }

  private synchronized void addStepMouseOverDelayTimer( final StepMeta stepMeta ) {

    // Don't add the same mouse over delay timer twice...
    //
    if ( mouseOverSteps.contains( stepMeta ) ) {
      return;
    }

    mouseOverSteps.add( stepMeta );

    DelayTimer delayTimer = new DelayTimer( 500, new DelayListener() {
      @Override
      public void expired() {
        mouseOverSteps.remove( stepMeta );
        delayTimers.remove( stepMeta );
        showTargetStreamsStep = null;
        asyncRedraw();
      }
    }, new Callable<Boolean>() {

      @Override
      public Boolean call() throws Exception {
        Point cursor = getLastMove();
        if ( cursor != null ) {
          AreaOwner areaOwner = getVisibleAreaOwner( cursor.x, cursor.y );
          if ( areaOwner != null ) {
            AreaType areaType = areaOwner.getAreaType();
            if ( areaType == AreaType.STEP_ICON ) {
              StepMeta selectedStepMeta = (StepMeta) areaOwner.getOwner();
              return selectedStepMeta == stepMeta;
            } else if ( areaType != null && areaType.belongsToTransContextMenu() ) {
              StepMeta selectedStepMeta = (StepMeta) areaOwner.getParent();
              return selectedStepMeta == stepMeta;
            } else if ( areaOwner.getExtensionAreaType() != null ) {
              return true;
            }
          }
        }
        return false;
      }
    } );

    new Thread( delayTimer ).start();

    delayTimers.put( stepMeta, delayTimer );
  }

  private void stopStepMouseOverDelayTimer( final StepMeta stepMeta ) {
    DelayTimer delayTimer = delayTimers.get( stepMeta );
    if ( delayTimer != null ) {
      delayTimer.stop();
    }
  }

  private void stopStepMouseOverDelayTimers() {
    for ( DelayTimer timer : delayTimers.values() ) {
      timer.stop();
    }
  }

  protected void asyncRedraw() {
    spoon.getDisplay().asyncExec( new Runnable() {
      @Override
      public void run() {
        if ( !TransGraph.this.isDisposed() ) {
          TransGraph.this.redraw();
        }
      }
    } );
  }

  private void addToolBar() {

    try {
      toolbar = (XulToolbar) getXulDomContainer().getDocumentRoot().getElementById( "nav-toolbar" );

      ToolBar swtToolbar = (ToolBar) toolbar.getManagedObject();
      swtToolbar.setBackground( GUIResource.getInstance().getColorDemoGray() );
      swtToolbar.pack();

      // Added 1/11/2016 to implement dropdown option for "Run"
      ToolItem runItem = new ToolItem( swtToolbar, SWT.DROP_DOWN, 0 );

      runItem.setImage( GUIResource.getInstance().getImage( "ui/images/run.svg" ) );
      runItem.setToolTipText( BaseMessages.getString( PKG, "Spoon.Tooltip.RunTranformation" ) );
      runItem.addSelectionListener( new SelectionAdapter() {

        @Override
        public void widgetSelected( SelectionEvent e ) {
          if ( e.detail == SWT.DROP_DOWN ) {
            Menu menu = new Menu( shell, SWT.POP_UP );

            MenuItem item1 = new MenuItem( menu, SWT.PUSH );
            item1.setText( BaseMessages.getString( PKG, "Spoon.Run.Run" ) );
            item1.setAccelerator( SWT.F9 );
            item1.addSelectionListener( new SelectionAdapter() {
              @Override
              public void widgetSelected( SelectionEvent e1 ) {
                runTransformation();
              }
            } );

            MenuItem item2 = new MenuItem( menu, SWT.PUSH );
            item2.setText( BaseMessages.getString( PKG, "Spoon.Run.RunOptions" ) );
            item2.setAccelerator( SWT.F8 );
            item2.addSelectionListener( new SelectionAdapter() {
              @Override
              public void widgetSelected( SelectionEvent e2 ) {
                runOptionsTransformation();
              }
            } );
            menu.setLocation( shell.getDisplay().map( mainComposite.getParent(), null, mainComposite.getLocation() ) );
            menu.setVisible( true );
          } else {
            runTransformation();
          }
        }
      } );

      stopItem = new ToolItem( swtToolbar, SWT.DROP_DOWN, 2 );

      stopItem.setImage( GUIResource.getInstance().getImage( "ui/images/stop.svg" ) );
      stopItem.setToolTipText( BaseMessages.getString( PKG, "Spoon.Tooltip.StopTranformation" ) );
      stopItem.addSelectionListener( new SelectionAdapter() {

        @Override
        public void widgetSelected( SelectionEvent e ) {
          if ( e.detail == SWT.DROP_DOWN ) {
            Menu menu = new Menu( shell, SWT.POP_UP );

            MenuItem item1 = new MenuItem( menu, SWT.PUSH );
            item1.setText( BaseMessages.getString( PKG, "Spoon.Menu.StopTranformation" ) );
            item1.addSelectionListener( new SelectionAdapter() {
              @Override
              public void widgetSelected( SelectionEvent e1 ) {
                stopTransformation();
              }
            } );

            MenuItem item2 = new MenuItem( menu, SWT.PUSH );
            item2.setText( BaseMessages.getString( PKG, "Spoon.Menu.SafeStopTranformation" ) );
            item2.addSelectionListener( new SelectionAdapter() {
              @Override
              public void widgetSelected( SelectionEvent e2 ) {
                safeStop();
              }
            } );
            menu.setLocation( shell.getDisplay().map( mainComposite.getParent(), null, mainComposite.getLocation() ) );
            menu.setVisible( true );
          } else {
            stopTransformation();
          }
        }
      } );

      // Hack alert : more XUL limitations...
      // TODO: no longer a limitation use toolbaritem
      //
      ToolItem sep = new ToolItem( swtToolbar, SWT.SEPARATOR );

      zoomLabel = new Combo( swtToolbar, SWT.DROP_DOWN );
      zoomLabel.setItems( TransPainter.magnificationDescriptions );
      zoomLabel.addSelectionListener( new SelectionAdapter() {
        @Override
        public void widgetSelected( SelectionEvent arg0 ) {
          readMagnification();
        }
      } );

      zoomLabel.addKeyListener( new KeyAdapter() {
        @Override
        public void keyPressed( KeyEvent event ) {
          if ( event.character == SWT.CR ) {
            readMagnification();
          }
        }
      } );

      setZoomLabel();
      zoomLabel.pack();

      sep.setWidth( 80 );
      sep.setControl( zoomLabel );
      swtToolbar.pack();
    } catch ( Throwable t ) {
      log.logError( "Error loading the navigation toolbar for Spoon", t );
      new ErrorDialog( shell, BaseMessages.getString( PKG, "Spoon.Exception.ErrorReadingXULFile.Title" ), BaseMessages
        .getString( PKG, "Spoon.Exception.ErrorReadingXULFile.Message", XUL_FILE_TRANS_TOOLBAR ), new Exception( t ) );
    }
  }

  /**
   * Allows for magnifying to any percentage entered by the user...
   */
  private void readMagnification() {
    String possibleText = zoomLabel.getText();
    possibleText = possibleText.replace( "%", "" );

    float possibleFloatMagnification;
    try {
      possibleFloatMagnification = Float.parseFloat( possibleText ) / 100;
      magnification = possibleFloatMagnification;
      if ( zoomLabel.getText().indexOf( '%' ) < 0 ) {
        zoomLabel.setText( zoomLabel.getText().concat( "%" ) );
      }
    } catch ( Exception e ) {
      modalMessageDialog( getString( "TransGraph.Dialog.InvalidZoomMeasurement.Title" ),
        getString( "TransGraph.Dialog.InvalidZoomMeasurement.Message", zoomLabel.getText() ),
        SWT.YES | SWT.ICON_ERROR );
    }
    redraw();
  }

  protected void hideToolTips() {
    toolTip.hide();
    helpTip.hide();
    toolTip.setHideDelay( TOOLTIP_HIDE_DELAY_SHORT );
  }

  private void showHelpTip( int x, int y, String tipTitle, String tipMessage ) {

    helpTip.setTitle( tipTitle );
    helpTip.setMessage( tipMessage.replaceAll( "\n", Const.CR ) );
    helpTip
      .setCheckBoxMessage( BaseMessages.getString( PKG, "TransGraph.HelpToolTip.DoNotShowAnyMoreCheckBox.Message" ) );

    // helpTip.hide();
    // int iconSize = spoon.props.getIconSize();
    org.eclipse.swt.graphics.Point location = new org.eclipse.swt.graphics.Point( x - 5, y - 5 );

    helpTip.show( location );
  }

  /**
   * Select all the steps in a certain (screen) rectangle
   *
   * @param rect The selection area as a rectangle
   */
  public void selectInRect( TransMeta transMeta, org.pentaho.di.core.gui.Rectangle rect ) {
    if ( rect.height < 0 || rect.width < 0 ) {
      org.pentaho.di.core.gui.Rectangle rectified =
        new org.pentaho.di.core.gui.Rectangle( rect.x, rect.y, rect.width, rect.height );

      // Only for people not dragging from left top to right bottom
      if ( rectified.height < 0 ) {
        rectified.y = rectified.y + rectified.height;
        rectified.height = -rectified.height;
      }
      if ( rectified.width < 0 ) {
        rectified.x = rectified.x + rectified.width;
        rectified.width = -rectified.width;
      }
      rect = rectified;
    }

    for ( int i = 0; i < transMeta.nrSteps(); i++ ) {
      StepMeta stepMeta = transMeta.getStep( i );
      Point a = stepMeta.getLocation();
      if ( rect.contains( a.x, a.y ) ) {
        stepMeta.setSelected( true );
      }
    }

    for ( int i = 0; i < transMeta.nrNotes(); i++ ) {
      NotePadMeta ni = transMeta.getNote( i );
      Point a = ni.getLocation();
      Point b = new Point( a.x + ni.width, a.y + ni.height );
      if ( rect.contains( a.x, a.y ) && rect.contains( b.x, b.y ) ) {
        ni.setSelected( true );
      }
    }
  }

  @Override
  public void keyPressed( KeyEvent e ) {

    if ( e.keyCode == SWT.ESC ) {
      clearSettings();
      redraw();
    }

    if ( e.keyCode == SWT.DEL ) {
      List<StepMeta> stepMeta = transMeta.getSelectedSteps();
      if ( stepMeta != null && stepMeta.size() > 0 ) {
        delSelected( null );
      }
    }

    if ( e.keyCode == SWT.F1 ) {
      spoon.browseVersionHistory();
    }

    if ( e.keyCode == SWT.F2 ) {
      spoon.editKettlePropertiesFile();
    }

    // CTRL-UP : allignTop();
    if ( e.keyCode == SWT.ARROW_UP && ( e.stateMask & SWT.MOD1 ) != 0 ) {
      alligntop();
    }
    // CTRL-DOWN : allignBottom();
    if ( e.keyCode == SWT.ARROW_DOWN && ( e.stateMask & SWT.MOD1 ) != 0 ) {
      allignbottom();
    }
    // CTRL-LEFT : allignleft();
    if ( e.keyCode == SWT.ARROW_LEFT && ( e.stateMask & SWT.MOD1 ) != 0 ) {
      allignleft();
    }
    // CTRL-RIGHT : allignRight();
    if ( e.keyCode == SWT.ARROW_RIGHT && ( e.stateMask & SWT.MOD1 ) != 0 ) {
      allignright();
    }
    // ALT-RIGHT : distributeHorizontal();
    if ( e.keyCode == SWT.ARROW_RIGHT && ( e.stateMask & SWT.ALT ) != 0 ) {
      distributehorizontal();
    }
    // ALT-UP : distributeVertical();
    if ( e.keyCode == SWT.ARROW_UP && ( e.stateMask & SWT.ALT ) != 0 ) {
      distributevertical();
    }
    // ALT-HOME : snap to grid
    if ( e.keyCode == SWT.HOME && ( e.stateMask & SWT.ALT ) != 0 ) {
      snaptogrid( ConstUI.GRID_SIZE );
    }

    if ( e.character == 'E' && ( e.stateMask & SWT.CTRL ) != 0 ) {
      checkErrorVisuals();
    }

    // CTRL-W or CTRL-F4 : close tab
    if ( ( e.keyCode == 'w' && ( e.stateMask & SWT.MOD1 ) != 0 )
      || ( e.keyCode == SWT.F4 && ( e.stateMask & SWT.MOD1 ) != 0 ) ) {
      spoon.tabCloseSelected();
    }

    // Auto-layout
    if ( e.character == 'A' ) {
      autoLayout();
    }

    // SPACE : over a step: show output fields...
    if ( e.character == ' ' && lastMove != null ) {

      Point real = lastMove;

      // Hide the tooltip!
      hideToolTips();

      // Set the pop-up menu
      StepMeta stepMeta = transMeta.getStep( real.x, real.y, iconsize );
      if ( stepMeta != null ) {
        // OK, we found a step, show the output fields...
        inputOutputFields( stepMeta, false );
      }
    }

  }

  @Override
  public void keyReleased( KeyEvent e ) {
  }

  @Override
  public boolean setFocus() {
    return ( canvas != null ) ? canvas.setFocus() : false;
  }

  public void renameStep( StepMeta stepMeta, String stepname ) {
    String newname = stepname;

    StepMeta smeta = transMeta.findStep( newname, stepMeta );
    int nr = 2;
    while ( smeta != null ) {
      newname = stepname + " " + nr;
      smeta = transMeta.findStep( newname );
      nr++;
    }
    if ( nr > 2 ) {
      stepname = newname;
      modalMessageDialog( getString( "Spoon.Dialog.StepnameExists.Title" ),
        getString( "Spoon.Dialog.StepnameExists.Message", stepname ), SWT.OK | SWT.ICON_INFORMATION );
    }
    stepMeta.setName( stepname );
    stepMeta.setChanged();
    spoon.refreshTree(); // to reflect the new name
    spoon.refreshGraph();
  }

  public void clearSettings() {
    selectedStep = null;
    noInputStep = null;
    selectedNote = null;
    selectedSteps = null;
    selectionRegion = null;
    candidate = null;
    last_hop_split = null;
    lastButton = 0;
    iconoffset = null;
    startHopStep = null;
    endHopStep = null;
    endHopLocation = null;
    mouseOverSteps.clear();
    for ( int i = 0; i < transMeta.nrTransHops(); i++ ) {
      // CHECKSTYLE:Indentation:OFF
      transMeta.getTransHop( i ).split = false;
    }

    stopStepMouseOverDelayTimers();
  }

  public String[] getDropStrings( String str, String sep ) {
    StringTokenizer strtok = new StringTokenizer( str, sep );
    String[] retval = new String[ strtok.countTokens() ];
    int i = 0;
    while ( strtok.hasMoreElements() ) {
      retval[ i ] = strtok.nextToken();
      i++;
    }
    return retval;
  }

  public Point getRealPosition( Composite canvas, int x, int y ) {
    Point p = new Point( 0, 0 );
    Composite follow = canvas;
    while ( follow != null ) {
      org.eclipse.swt.graphics.Point loc = follow.getLocation();
      Point xy = new Point( loc.x, loc.y );
      p.x += xy.x;
      p.y += xy.y;
      follow = follow.getParent();
    }

    int offsetX = -16;
    int offsetY = -64;
    if ( Const.isOSX() ) {
      offsetX = -2;
      offsetY = -24;
    }
    p.x = x - p.x + offsetX;
    p.y = y - p.y + offsetY;

    return screen2real( p.x, p.y );
  }

  /**
   * See if location (x,y) is on a line between two steps: the hop!
   *
   * @param x
   * @param y
   * @return the transformation hop on the specified location, otherwise: null
   */
  protected TransHopMeta findHop( int x, int y ) {
    return findHop( x, y, null );
  }

  /**
   * See if location (x,y) is on a line between two steps: the hop!
   *
   * @param x
   * @param y
   * @param exclude the step to exclude from the hops (from or to location). Specify null if no step is to be excluded.
   * @return the transformation hop on the specified location, otherwise: null
   */
  private TransHopMeta findHop( int x, int y, StepMeta exclude ) {
    int i;
    TransHopMeta online = null;
    for ( i = 0; i < transMeta.nrTransHops(); i++ ) {
      TransHopMeta hi = transMeta.getTransHop( i );
      StepMeta fs = hi.getFromStep();
      StepMeta ts = hi.getToStep();

      if ( fs == null || ts == null ) {
        return null;
      }

      // If either the "from" or "to" step is excluded, skip this hop.
      //
      if ( exclude != null && ( exclude.equals( fs ) || exclude.equals( ts ) ) ) {
        continue;
      }

      int[] line = getLine( fs, ts );

      if ( pointOnLine( x, y, line ) ) {
        online = hi;
      }
    }
    return online;
  }

  private int[] getLine( StepMeta fs, StepMeta ts ) {
    Point from = fs.getLocation();
    Point to = ts.getLocation();
    offset = getOffset();

    int x1 = from.x + iconsize / 2;
    int y1 = from.y + iconsize / 2;

    int x2 = to.x + iconsize / 2;
    int y2 = to.y + iconsize / 2;

    return new int[] { x1, y1, x2, y2 };
  }

  public void hideStep() {
    for ( int i = 0; i < transMeta.nrSteps(); i++ ) {
      StepMeta sti = transMeta.getStep( i );
      if ( sti.isDrawn() && sti.isSelected() ) {
        sti.hideStep();
        spoon.refreshTree();
      }
    }
    getCurrentStep().hideStep();
    spoon.refreshTree();
    redraw();
  }

  public void checkSelectedSteps() {
    spoon.checkTrans( transMeta, true );
  }

  public void detachStep() {
    detach( getCurrentStep() );
    selectedSteps = null;
  }

  public void generateMappingToThisStep() {
    spoon.generateFieldMapping( transMeta, getCurrentStep() );
  }

  public void partitioning() {
    spoon.editPartitioning( transMeta, getCurrentStep() );
  }

  public void clustering() {
    List<StepMeta> selected = transMeta.getSelectedSteps();
    if ( selected != null && selected.size() > 0 ) {
      spoon.editClustering( transMeta, transMeta.getSelectedSteps() );
    } else {
      spoon.editClustering( transMeta, getCurrentStep() );
    }
  }

  public void errorHandling() {
    spoon.editStepErrorHandling( transMeta, getCurrentStep() );
  }

  public void newHopChoice() {
    selectedSteps = null;
    newHop();
  }

  public void editStep() {
    selectedSteps = null;
    editStep( getCurrentStep() );
  }

  public void editDescription() {
    editDescription( getCurrentStep() );
  }

  public void setDistributes() {
    getCurrentStep().setDistributes( true );
    getCurrentStep().setRowDistribution( null );
    spoon.refreshGraph();
    spoon.refreshTree();
  }

  public void setCustomRowDistribution() {
    // TODO: ask user which row distribution is needed...
    //
    RowDistributionInterface rowDistribution = askUserForCustomDistributionMethod();
    getCurrentStep().setDistributes( true );
    getCurrentStep().setRowDistribution( rowDistribution );
    spoon.refreshGraph();
    spoon.refreshTree();
  }

  public RowDistributionInterface askUserForCustomDistributionMethod() {
    List<PluginInterface> plugins = PluginRegistry.getInstance().getPlugins( RowDistributionPluginType.class );
    if ( Utils.isEmpty( plugins ) ) {
      return null;
    }
    List<String> choices = new ArrayList<>();
    for ( PluginInterface plugin : plugins ) {
      choices.add( plugin.getName() + " : " + plugin.getDescription() );
    }
    EnterSelectionDialog dialog =
      new EnterSelectionDialog( shell, choices.toArray( new String[ choices.size() ] ), "Select distribution method",
        "Please select the row distribution method:" );
    if ( dialog.open() != null ) {
      PluginInterface plugin = plugins.get( dialog.getSelectionNr() );
      try {
        return (RowDistributionInterface) PluginRegistry.getInstance().loadClass( plugin );
      } catch ( Exception e ) {
        new ErrorDialog( shell, "Error", "Error loading row distribution plugin class", e );
        return null;
      }
    } else {
      return null;
    }
  }

  public void setCopies() {
    getCurrentStep().setDistributes( false );
    spoon.refreshGraph();
    spoon.refreshTree();
  }

  public void copies() {
    copies( getCurrentStep() );
  }

  public void copies( StepMeta stepMeta ) {
    final boolean multipleOK = checkNumberOfCopies( transMeta, stepMeta );
    selectedSteps = null;
    String tt = BaseMessages.getString( PKG, "TransGraph.Dialog.NrOfCopiesOfStep.Title" );
    String mt = BaseMessages.getString( PKG, "TransGraph.Dialog.NrOfCopiesOfStep.Message" );
    EnterStringDialog nd = new EnterStringDialog( shell, stepMeta.getCopiesString(), tt, mt, true, transMeta );
    String cop = nd.open();
    if ( !Utils.isEmpty( cop ) ) {

      int copies = Const.toInt( transMeta.environmentSubstitute( cop ), -1 );
      if ( copies > 1 && !multipleOK ) {
        cop = "1";

        modalMessageDialog( getString( "TransGraph.Dialog.MultipleCopiesAreNotAllowedHere.Title" ),
          getString( "TransGraph.Dialog.MultipleCopiesAreNotAllowedHere.Message" ), SWT.YES | SWT.ICON_WARNING );
      }
      String cps = stepMeta.getCopiesString();
      if ( ( cps != null && !cps.equals( cop ) ) || ( cps == null && cop != null ) ) {
        stepMeta.setChanged();
      }
      stepMeta.setCopiesString( cop );
      spoon.refreshGraph();
    }
  }

  public void dupeStep() {
    try {
      List<StepMeta> steps = transMeta.getSelectedSteps();
      if ( steps.size() <= 1 ) {
        spoon.dupeStep( transMeta, getCurrentStep() );
      } else {
        for ( StepMeta stepMeta : steps ) {
          spoon.dupeStep( transMeta, stepMeta );
        }
      }
    } catch ( Exception ex ) {
      new ErrorDialog( shell, BaseMessages.getString( PKG, "TransGraph.Dialog.ErrorDuplicatingStep.Title" ),
        BaseMessages.getString( PKG, "TransGraph.Dialog.ErrorDuplicatingStep.Message" ), ex );
    }
  }

  public void copyStep() {
    spoon.copySelected( transMeta, transMeta.getSelectedSteps(), transMeta.getSelectedNotes() );
  }

  public void delSelected() {
    delSelected( getCurrentStep() );
  }

  public void fieldsBefore() {
    selectedSteps = null;
    inputOutputFields( getCurrentStep(), true );
  }

  public void fieldsAfter() {
    selectedSteps = null;
    inputOutputFields( getCurrentStep(), false );
  }

  public void fieldsLineage() {
    TransDataLineage tdl = new TransDataLineage( transMeta );
    try {
      tdl.calculateLineage();
    } catch ( Exception e ) {
      new ErrorDialog( shell, "Lineage error", "Unexpected lineage calculation error", e );
    }
  }

  public void editHop() {
    selectionRegion = null;
    editHop( getCurrentHop() );
  }

  public void flipHopDirection() {
    selectionRegion = null;
    TransHopMeta hi = getCurrentHop();

    hi.flip();
    if ( transMeta.hasLoop( hi.getToStep() ) ) {
      spoon.refreshGraph();
      modalMessageDialog( getString( "TransGraph.Dialog.LoopsAreNotAllowed.Title" ),
        getString( "TransGraph.Dialog.LoopsAreNotAllowed.Message" ), SWT.OK | SWT.ICON_ERROR );
      hi.flip();
      spoon.refreshGraph();
    } else {
      hi.setChanged();
      spoon.refreshGraph();
      spoon.refreshTree();
      spoon.setShellText();
    }
  }

  public void enableHop() {
    selectionRegion = null;
    TransHopMeta hi = getCurrentHop();
    TransHopMeta before = (TransHopMeta) hi.clone();
    setHopEnabled( hi, !hi.isEnabled() );
    if ( hi.isEnabled() && transMeta.hasLoop( hi.getToStep() ) ) {
      setHopEnabled( hi, false );
      modalMessageDialog( getString( "TransGraph.Dialog.LoopAfterHopEnabled.Title" ),
        getString( "TransGraph.Dialog.LoopAfterHopEnabled.Message" ), SWT.OK | SWT.ICON_ERROR );
    } else {
      TransHopMeta after = (TransHopMeta) hi.clone();
      spoon.addUndoChange( transMeta, new TransHopMeta[] { before }, new TransHopMeta[] { after },
        new int[] { transMeta.indexOfTransHop( hi ) } );
      spoon.refreshGraph();
      spoon.refreshTree();
    }
    updateErrorMetaForHop( hi );
  }

  public void deleteHop() {
    selectionRegion = null;
    TransHopMeta hi = getCurrentHop();
    spoon.delHop( transMeta, hi );
  }

  private void updateErrorMetaForHop( TransHopMeta hop ) {
    if ( hop != null && hop.isErrorHop() ) {
      StepErrorMeta errorMeta = hop.getFromStep().getStepErrorMeta();
      if ( errorMeta != null ) {
        errorMeta.setEnabled( hop.isEnabled() );
      }
    }
  }

  public void enableHopsBetweenSelectedSteps() {
    enableHopsBetweenSelectedSteps( true );
  }

  public void disableHopsBetweenSelectedSteps() {
    enableHopsBetweenSelectedSteps( false );
  }

  /**
   * This method enables or disables all the hops between the selected steps.
   **/
  public void enableHopsBetweenSelectedSteps( boolean enabled ) {
    List<StepMeta> list = transMeta.getSelectedSteps();

    boolean hasLoop = false;

    for ( int i = 0; i < transMeta.nrTransHops(); i++ ) {
      TransHopMeta hop = transMeta.getTransHop( i );
      if ( list.contains( hop.getFromStep() ) && list.contains( hop.getToStep() ) ) {

        TransHopMeta before = (TransHopMeta) hop.clone();
        setHopEnabled( hop, enabled );
        TransHopMeta after = (TransHopMeta) hop.clone();
        spoon.addUndoChange( transMeta, new TransHopMeta[] { before }, new TransHopMeta[] { after },
          new int[] { transMeta.indexOfTransHop( hop ) } );
        if ( transMeta.hasLoop( hop.getToStep() ) ) {
          hasLoop = true;
          setHopEnabled( hop, false );
        }
      }
    }

    if ( enabled && hasLoop ) {
      modalMessageDialog( getString( "TransGraph.Dialog.HopCausesLoop.Title" ),
        getString( "TransGraph.Dialog.HopCausesLoop.Message" ), SWT.OK | SWT.ICON_ERROR );
    }

    spoon.refreshGraph();
  }

  public void enableHopsDownstream() {
    enableDisableHopsDownstream( true );
  }

  public void disableHopsDownstream() {
    enableDisableHopsDownstream( false );
  }

  public void enableDisableHopsDownstream( boolean enabled ) {
    if ( currentHop == null ) {
      return;
    }

    TransHopMeta before = (TransHopMeta) currentHop.clone();
    setHopEnabled( currentHop, enabled );
    TransHopMeta after = (TransHopMeta) currentHop.clone();
    spoon.addUndoChange( transMeta, new TransHopMeta[] { before }, new TransHopMeta[] { after }, new int[] { transMeta
      .indexOfTransHop( currentHop ) } );

    Set<StepMeta> checkedEntries = enableDisableNextHops( currentHop.getToStep(), enabled, new HashSet<>() );

    if ( checkedEntries.stream().anyMatch( entry -> transMeta.hasLoop( entry ) ) ) {
      modalMessageDialog( getString( "TransGraph.Dialog.HopCausesLoop.Title" ),
        getString( "TransGraph.Dialog.HopCausesLoop.Message" ), SWT.OK | SWT.ICON_ERROR );
    }

    spoon.refreshGraph();
  }

  private Set<StepMeta> enableDisableNextHops( StepMeta from, boolean enabled, Set<StepMeta> checkedEntries ) {
    checkedEntries.add( from );
    transMeta.getTransHops().stream()
      .filter( hop -> from.equals( hop.getFromStep() ) )
      .forEach( hop -> {
        if ( hop.isEnabled() != enabled ) {
          TransHopMeta before = (TransHopMeta) hop.clone();
          setHopEnabled( hop, enabled );
          TransHopMeta after = (TransHopMeta) hop.clone();
          spoon.addUndoChange( transMeta, new TransHopMeta[] { before }, new TransHopMeta[] { after },
            new int[] { transMeta
              .indexOfTransHop( hop ) } );
        }
        if ( !checkedEntries.contains( hop.getToStep() ) ) {
          enableDisableNextHops( hop.getToStep(), enabled, checkedEntries );
        }
      } );
    return checkedEntries;
  }

  public void editNote() {
    selectionRegion = null;
    editNote( getCurrentNote() );
  }

  public void deleteNote() {
    selectionRegion = null;
    int idx = transMeta.indexOfNote( ni );
    if ( idx >= 0 ) {
      transMeta.removeNote( idx );
      spoon.addUndoDelete( transMeta, new NotePadMeta[] { (NotePadMeta) ni.clone() }, new int[] { idx } );
      redraw();
    }
  }

  public void raiseNote() {
    selectionRegion = null;
    int idx = transMeta.indexOfNote( getCurrentNote() );
    if ( idx >= 0 ) {
      transMeta.raiseNote( idx );
      // TBD: spoon.addUndoRaise(transMeta, new NotePadMeta[] {getCurrentNote()}, new int[] {idx} );
    }
    redraw();
  }

  public void lowerNote() {
    selectionRegion = null;
    int idx = transMeta.indexOfNote( getCurrentNote() );
    if ( idx >= 0 ) {
      transMeta.lowerNote( idx );
      // TBD: spoon.addUndoLower(transMeta, new NotePadMeta[] {getCurrentNote()}, new int[] {idx} );
    }
    redraw();
  }

  public void newNote() {
    selectionRegion = null;
    String title = BaseMessages.getString( PKG, "TransGraph.Dialog.NoteEditor.Title" );
    NotePadDialog dd = new NotePadDialog( transMeta, shell, title );
    NotePadMeta n = dd.open();
    if ( n != null ) {
      NotePadMeta npi =
        new NotePadMeta( n.getNote(), lastclick.x, lastclick.y, ConstUI.NOTE_MIN_SIZE, ConstUI.NOTE_MIN_SIZE, n
          .getFontName(), n.getFontSize(), n.isFontBold(), n.isFontItalic(), n.getFontColorRed(), n
          .getFontColorGreen(), n.getFontColorBlue(), n.getBackGroundColorRed(), n.getBackGroundColorGreen(), n
          .getBackGroundColorBlue(), n.getBorderColorRed(), n.getBorderColorGreen(), n.getBorderColorBlue(), n
          .isDrawShadow() );
      transMeta.addNote( npi );
      spoon.addUndoNew( transMeta, new NotePadMeta[] { npi }, new int[] { transMeta.indexOfNote( npi ) } );
      redraw();
    }
  }

  public void paste() {
    final String clipcontent = spoon.fromClipboard();
    Point loc = new Point( currentMouseX, currentMouseY );
    spoon.pasteXML( transMeta, clipcontent, loc );
  }

  public void settings() {
    editProperties( transMeta, spoon, spoon.getRepository(), true );
  }

  public void newStep( String description ) {
    StepMeta stepMeta = spoon.newStep( transMeta, description, description, false, true );
    PropsUI.setLocation( stepMeta, currentMouseX, currentMouseY );
    stepMeta.setDraw( true );
    redraw();
  }

  /**
   * This sets the popup-menu on the background of the canvas based on the xy coordinate of the mouse. This method is
   * called after a mouse-click.
   *
   * @param x X-coordinate on screen
   * @param y Y-coordinate on screen
   */
  private synchronized void setMenu( int x, int y ) {
    try {
      currentMouseX = x;
      currentMouseY = y;

      final StepMeta stepMeta = transMeta.getStep( x, y, iconsize );
      if ( stepMeta != null ) { // We clicked on a Step!

        setCurrentStep( stepMeta );

        XulMenupopup menu = menuMap.get( "trans-graph-entry" );

        try {
          ExtensionPointHandler.callExtensionPoint( LogChannel.GENERAL, KettleExtensionPoint.TransStepRightClick.id,
            new StepMenuExtension( this, menu ) );
        } catch ( Exception ex ) {
          LogChannel.GENERAL.logError( "Error calling TransStepRightClick extension point", ex );
        }

        if ( menu != null ) {
          List<StepMeta> selection = transMeta.getSelectedSteps();
          doRightClickSelection( stepMeta, selection );
          int sels = selection.size();

          Document doc = getXulDomContainer().getDocumentRoot();

          // TODO: cache the next line (seems fast enough)?
          //
          List<PluginInterface> rowDistributionPlugins =
            PluginRegistry.getInstance().getPlugins( RowDistributionPluginType.class );

          JfaceMenupopup customRowDistMenu =
            (JfaceMenupopup) doc.getElementById( "trans-graph-entry-data-movement-popup" );
          customRowDistMenu.setDisabled( false );
          customRowDistMenu.removeChildren();

          // Add the default round robin plugin...
          //
          Action action = new Action( "RoundRobinRowDistribution", Action.AS_CHECK_BOX ) {
            @Override
            public void run() {
              stepMeta.setRowDistribution( null ); // default
              stepMeta.setDistributes( true );
            }
          };
          boolean selected = stepMeta.isDistributes() && stepMeta.getRowDistribution() == null;
          action.setChecked( selected );
          JfaceMenuitem child =
            new JfaceMenuitem( null, customRowDistMenu, xulDomContainer, "Round Robin row distribution", 0, action );
          child.setLabel( BaseMessages.getString( PKG, "TransGraph.PopupMenu.RoundRobin" ) );
          child.setDisabled( false );
          child.setSelected( selected );

          for ( int p = 0; p < rowDistributionPlugins.size(); p++ ) {
            final PluginInterface rowDistributionPlugin = rowDistributionPlugins.get( p );
            selected =
              stepMeta.isDistributes() && stepMeta.getRowDistribution() != null
                && stepMeta.getRowDistribution().getCode().equals( rowDistributionPlugin.getIds()[ 0 ] );

            action = new Action( rowDistributionPlugin.getIds()[ 0 ], Action.AS_CHECK_BOX ) {
              @Override
              public void run() {
                try {
                  stepMeta.setRowDistribution( (RowDistributionInterface) PluginRegistry.getInstance().loadClass(
                    rowDistributionPlugin ) );
                } catch ( Exception e ) {
                  LogChannel.GENERAL.logError( "Error loading row distribution plugin class: ", e );
                }
              }
            };
            action.setChecked( selected );
            child =
              new JfaceMenuitem( null, customRowDistMenu, xulDomContainer, rowDistributionPlugin.getName(), p + 1,
                action );
            child.setLabel( rowDistributionPlugin.getName() );
            child.setDisabled( false );
            child.setSelected( selected );
          }

          // Add the default copy rows plugin...
          //

          action = new Action( "CopyRowsDistribution", Action.AS_CHECK_BOX ) {
            @Override
            public void run() {
              stepMeta.setDistributes( false );
            }
          };
          selected = !stepMeta.isDistributes();
          action.setChecked( selected );
          child = new JfaceMenuitem( null, customRowDistMenu, xulDomContainer, "Copy rows distribution", 0, action );
          child.setLabel( BaseMessages.getString( PKG, "TransGraph.PopupMenu.CopyData" ) );
          child.setDisabled( false );
          child.setSelected( selected );
          JfaceMenupopup launchMenu = (JfaceMenupopup) doc.getElementById( "trans-graph-entry-launch-popup" );
          String[] referencedObjects = stepMeta.getStepMetaInterface().getReferencedObjectDescriptions();
          boolean[] enabledObjects = stepMeta.getStepMetaInterface().isReferencedObjectEnabled();
          launchMenu.setDisabled( Utils.isEmpty( referencedObjects ) );

          launchMenu.removeChildren();
          int childIndex = 0;

          // First see if we need to add a special "active" entry (running transformation)
          //
          StepMetaInterface stepMetaInterface = stepMeta.getStepMetaInterface();
          String activeReferencedObjectDescription = stepMetaInterface.getActiveReferencedObjectDescription();
          if ( getActiveSubtransformation( this, stepMeta ) != null && activeReferencedObjectDescription != null ) {
            action = new Action( activeReferencedObjectDescription, Action.AS_DROP_DOWN_MENU ) {
              @Override
              public void run() {
                openMapping( stepMeta, -1 ); // negative by convention
              }
            };
            child =
              new JfaceMenuitem( null, launchMenu, xulDomContainer, activeReferencedObjectDescription, childIndex++,
                action );
            child.setLabel( activeReferencedObjectDescription );
            child.setDisabled( false );
          }

          if ( !Utils.isEmpty( referencedObjects ) ) {
            for ( int i = 0; i < referencedObjects.length; i++ ) {
              final int index = i;
              String referencedObject = referencedObjects[ i ];
              action = new Action( referencedObject, Action.AS_DROP_DOWN_MENU ) {
                @Override
                public void run() {
                  openMapping( stepMeta, index );
                }
              };
              child = new JfaceMenuitem( null, launchMenu, xulDomContainer, referencedObject, childIndex++, action );
              child.setLabel( referencedObject );
              child.setDisabled( !enabledObjects[ i ] );
            }
          }

          initializeXulMenu( doc, selection, stepMeta );
          ConstUI.displayMenu( menu, canvas );
        }
      } else {
        final TransHopMeta hi = findHop( x, y );
        if ( hi != null ) { // We clicked on a HOP!

          XulMenupopup menu = menuMap.get( "trans-graph-hop" );
          if ( menu != null ) {
            setCurrentHop( hi );
            XulMenuitem item =
              (XulMenuitem) getXulDomContainer().getDocumentRoot().getElementById( "trans-graph-hop-enabled" );
            if ( item != null ) {
              if ( hi.isEnabled() ) {
                item.setLabel( BaseMessages.getString( PKG, "TransGraph.PopupMenu.DisableHop" ) );
              } else {
                item.setLabel( BaseMessages.getString( PKG, "TransGraph.PopupMenu.EnableHop" ) );
              }
            }

            ConstUI.displayMenu( menu, canvas );
          }
        } else {
          // Clicked on the background: maybe we hit a note?
          final NotePadMeta ni = transMeta.getNote( x, y );
          setCurrentNote( ni );
          if ( ni != null ) {

            XulMenupopup menu = menuMap.get( "trans-graph-note" );
            if ( menu != null ) {
              ConstUI.displayMenu( menu, canvas );
            }
          } else {
            XulMenupopup menu = menuMap.get( "trans-graph-background" );
            if ( menu != null ) {
              final String clipcontent = spoon.fromClipboard();
              XulMenuitem item =
                (XulMenuitem) getXulDomContainer().getDocumentRoot().getElementById( "trans-graph-background-paste" );
              if ( item != null ) {
                item.setDisabled( clipcontent == null );
              }
              ConstUI.displayMenu( menu, canvas );
            }

          }
        }
      }
    } catch ( Throwable t ) {
      // TODO: fix this: log somehow, is IGNORED for now.
      t.printStackTrace();
    }
  }

  public void selectAll() {
    spoon.editSelectAll();
  }

  public void clearSelection() {
    spoon.editUnselectAll();
  }

  protected void initializeXulMenu( Document doc, List<StepMeta> selection, StepMeta stepMeta ) throws KettleException {
    XulMenuitem item = (XulMenuitem) doc.getElementById( "trans-graph-entry-newhop" );
    int sels = selection.size();
    item.setDisabled( sels != 2 );

    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-align-snap" );

    item.setAcceltext( "ALT-HOME" );
    item.setLabel( BaseMessages.getString( PKG, "TransGraph.PopupMenu.SnapToGrid" ) );
    item.setAccesskey( "alt-home" );

    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-open-mapping" );

    XulMenu men = (XulMenu) doc.getElementById( TRANS_GRAPH_ENTRY_SNIFF );
    men.setDisabled( trans == null || trans.isRunning() == false );
    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-sniff-input" );
    item.setDisabled( trans == null || trans.isRunning() == false );
    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-sniff-output" );
    item.setDisabled( trans == null || trans.isRunning() == false );
    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-sniff-error" );
    item.setDisabled( !( stepMeta.supportsErrorHandling() && stepMeta.getStepErrorMeta() != null
      && stepMeta.getStepErrorMeta().getTargetStep() != null && trans != null && trans.isRunning() ) );

    XulMenu aMenu = (XulMenu) doc.getElementById( TRANS_GRAPH_ENTRY_AGAIN );
    if ( aMenu != null ) {
      aMenu.setDisabled( sels < 2 );
    }

    // item = (XulMenuitem) doc.getElementById("trans-graph-entry-data-movement-distribute");
    // item.setSelected(stepMeta.isDistributes());

    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-partitioning" );
    item.setDisabled( spoon.getPartitionSchemasNames( transMeta ).isEmpty() );

    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-data-movement-copy" );
    item.setSelected( !stepMeta.isDistributes() );

    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-hide" );
    item.setDisabled( !( stepMeta.isDrawn() && !transMeta.isAnySelectedStepUsedInTransHops() ) );

    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-detach" );
    item.setDisabled( !transMeta.isStepUsedInTransHops( stepMeta ) );

    item = (XulMenuitem) doc.getElementById( "trans-graph-entry-errors" );
    item.setDisabled( !stepMeta.supportsErrorHandling() );
  }

  private boolean checkNumberOfCopies( TransMeta transMeta, StepMeta stepMeta ) {
    boolean enabled = true;
    List<StepMeta> prevSteps = transMeta.findPreviousSteps( stepMeta );
    for ( StepMeta prevStep : prevSteps ) {
      // See what the target steps are.
      // If one of the target steps is our original step, we can't start multiple copies
      //
      String[] targetSteps = prevStep.getStepMetaInterface().getStepIOMeta().getTargetStepnames();
      if ( targetSteps != null ) {
        for ( int t = 0; t < targetSteps.length && enabled; t++ ) {
          if ( !Utils.isEmpty( targetSteps[ t ] ) && targetSteps[ t ].equalsIgnoreCase( stepMeta.getName() ) ) {
            enabled = false;
          }
        }
      }
    }
    return enabled;
  }

  private AreaOwner setToolTip( int x, int y, int screenX, int screenY ) {
    AreaOwner subject = null;

    if ( !spoon.getProperties().showToolTips() ) {
      return subject;
    }

    canvas.setToolTipText( null );

    String newTip = null;
    Image tipImage = null;

    final TransHopMeta hi = findHop( x, y );
    // check the area owner list...
    //
    StringBuilder tip = new StringBuilder();
    AreaOwner areaOwner = getVisibleAreaOwner( x, y );
    AreaType areaType = null;
    if ( areaOwner != null && areaOwner.getAreaType() != null ) {
      areaType = areaOwner.getAreaType();
      switch ( areaType ) {
        case REMOTE_INPUT_STEP:
          StepMeta step = (StepMeta) areaOwner.getParent();
          tip.append( "Remote input steps:" ).append( Const.CR ).append( "-----------------------" ).append( Const.CR );
          for ( RemoteStep remoteStep : step.getRemoteInputSteps() ) {
            tip.append( remoteStep.toString() ).append( Const.CR );
          }
          break;
        case REMOTE_OUTPUT_STEP:
          step = (StepMeta) areaOwner.getParent();
          tip.append( "Remote output steps:" ).append( Const.CR ).append( "-----------------------" )
            .append( Const.CR );
          for ( RemoteStep remoteStep : step.getRemoteOutputSteps() ) {
            tip.append( remoteStep.toString() ).append( Const.CR );
          }
          break;
        case STEP_PARTITIONING:
          step = (StepMeta) areaOwner.getParent();
          tip.append( "Step partitioning:" ).append( Const.CR ).append( "-----------------------" ).append( Const.CR );
          tip.append( step.getStepPartitioningMeta().toString() ).append( Const.CR );
          if ( step.getTargetStepPartitioningMeta() != null ) {
            tip.append( Const.CR ).append( Const.CR ).append(
              "TARGET: " + step.getTargetStepPartitioningMeta().toString() ).append( Const.CR );
          }
          break;
        case STEP_ERROR_ICON:
          String log = (String) areaOwner.getParent();
          tip.append( log );
          tipImage = GUIResource.getInstance().getImageStepError();
          break;
        case STEP_ERROR_RED_ICON:
          String redLog = (String) areaOwner.getParent();
          tip.append( redLog );
          tipImage = GUIResource.getInstance().getImageRedStepError();
          break;
        case HOP_COPY_ICON:
          step = (StepMeta) areaOwner.getParent();
          tip.append( BaseMessages.getString( PKG, "TransGraph.Hop.Tooltip.HopTypeCopy", step.getName(), Const.CR ) );
          tipImage = GUIResource.getInstance().getImageCopyHop();
          break;
        case ROW_DISTRIBUTION_ICON:
          step = (StepMeta) areaOwner.getParent();
          tip.append( BaseMessages.getString( PKG, "TransGraph.Hop.Tooltip.RowDistribution", step.getName(), step
            .getRowDistribution() == null ? "" : step.getRowDistribution().getDescription() ) );
          tip.append( Const.CR );
          tipImage = GUIResource.getInstance().getImageBalance();
          break;
        case HOP_INFO_ICON:
          StepMeta from = (StepMeta) areaOwner.getParent();
          StepMeta to = (StepMeta) areaOwner.getOwner();
          tip.append( BaseMessages.getString( PKG, "TransGraph.Hop.Tooltip.HopTypeInfo", to.getName(), from.getName(),
            Const.CR ) );
          tipImage = GUIResource.getInstance().getImageInfoHop();
          break;
        case HOP_ERROR_ICON:
          from = (StepMeta) areaOwner.getParent();
          to = (StepMeta) areaOwner.getOwner();
          areaOwner.getOwner();
          tip.append( BaseMessages.getString( PKG, "TransGraph.Hop.Tooltip.HopTypeError", from.getName(), to.getName(),
            Const.CR ) );
          tipImage = GUIResource.getInstance().getImageErrorHop();
          break;
        case HOP_INFO_STEP_COPIES_ERROR:
          from = (StepMeta) areaOwner.getParent();
          to = (StepMeta) areaOwner.getOwner();
          tip.append( BaseMessages.getString( PKG, "TransGraph.Hop.Tooltip.InfoStepCopies", from.getName(), to
            .getName(), Const.CR ) );
          tipImage = GUIResource.getInstance().getImageStepError();
          break;
        case STEP_INPUT_HOP_ICON:
          // StepMeta subjectStep = (StepMeta) (areaOwner.getParent());
          tip.append( BaseMessages.getString( PKG, "TransGraph.StepInputConnector.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageHopInput();
          break;
        case STEP_OUTPUT_HOP_ICON:
          // subjectStep = (StepMeta) (areaOwner.getParent());
          tip.append( BaseMessages.getString( PKG, "TransGraph.StepOutputConnector.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageHopOutput();
          break;
        case STEP_INFO_HOP_ICON:
          // subjectStep = (StepMeta) (areaOwner.getParent());
          // StreamInterface stream = (StreamInterface) areaOwner.getOwner();
          StepIOMetaInterface ioMeta = (StepIOMetaInterface) areaOwner.getOwner();
          tip.append( BaseMessages.getString( PKG, "TransGraph.StepInfoConnector.Tooltip" ) + Const.CR
            + ioMeta.toString() );
          tipImage = GUIResource.getInstance().getImageHopOutput();
          break;
        case STEP_TARGET_HOP_ICON:
          StreamInterface stream = (StreamInterface) areaOwner.getOwner();
          tip.append( stream.getDescription() );
          tipImage = GUIResource.getInstance().getImageHopOutput();
          break;
        case STEP_ERROR_HOP_ICON:
          StepMeta stepMeta = (StepMeta) areaOwner.getParent();
          if ( stepMeta.supportsErrorHandling() ) {
            tip.append( BaseMessages.getString( PKG, "TransGraph.StepSupportsErrorHandling.Tooltip" ) );
          } else {
            tip.append( BaseMessages.getString( PKG, "TransGraph.StepDoesNotSupportsErrorHandling.Tooltip" ) );
          }
          tipImage = GUIResource.getInstance().getImageHopOutput();
          break;
        case STEP_EDIT_ICON:
          tip.append( BaseMessages.getString( PKG, "TransGraph.EditStep.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageEdit();
          break;
        case STEP_INJECT_ICON:
          Object injection = areaOwner.getOwner();
          if ( injection != null ) {
            tip.append( BaseMessages.getString( PKG, "TransGraph.StepInjectionSupported.Tooltip" ) );
          } else {
            tip.append( BaseMessages.getString( PKG, "TransGraph.StepInjectionNotSupported.Tooltip" ) );
          }
          tipImage = GUIResource.getInstance().getImageInject();
          break;
        case STEP_MENU_ICON:
          tip.append( BaseMessages.getString( PKG, "TransGraph.ShowMenu.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageContextMenu();
          break;
        case STEP_ICON:
          StepMeta iconStepMeta = (StepMeta) areaOwner.getOwner();
          if ( iconStepMeta.isDeprecated() ) { // only need tooltip if step is deprecated
            tip.append( BaseMessages.getString( PKG, "TransGraph.DeprecatedStep.Tooltip.Title" ) ).append( Const.CR );
            String tipNext = BaseMessages.getString( PKG, "TransGraph.DeprecatedStep.Tooltip.Message1",
              iconStepMeta.getName() );
            int length = tipNext.length() + 5;
            for ( int i = 0; i < length; i++ ) {
              tip.append( "-" );
            }
            tip.append( Const.CR ).append( tipNext ).append( Const.CR );
            tip.append( BaseMessages.getString( PKG, "TransGraph.DeprecatedStep.Tooltip.Message2",
              iconStepMeta.getSuggestion() ) );
            tipImage = GUIResource.getInstance().getImageDeprecated();
            toolTip.setHideDelay( TOOLTIP_HIDE_DELAY_LONG );
          }
          break;
        default:
          break;
      }
    }

    if ( hi != null && tip.length() == 0 ) { // We clicked on a HOP!
      // Set the tooltip for the hop:
      tip.append( Const.CR ).append( BaseMessages.getString( PKG, "TransGraph.Dialog.HopInfo" ) ).append(
        newTip = hi.toString() ).append( Const.CR );
    }

    if ( tip.length() == 0 ) {
      newTip = null;
    } else {
      newTip = tip.toString();
    }

    if ( newTip == null ) {
      toolTip.hide();
      if ( hi != null ) { // We clicked on a HOP!

        // Set the tooltip for the hop:
        newTip =
          BaseMessages.getString( PKG, "TransGraph.Dialog.HopInfo" )
            + Const.CR
            + BaseMessages.getString( PKG, "TransGraph.Dialog.HopInfo.SourceStep" )
            + " "
            + hi.getFromStep().getName()
            + Const.CR
            + BaseMessages.getString( PKG, "TransGraph.Dialog.HopInfo.TargetStep" )
            + " "
            + hi.getToStep().getName()
            + Const.CR
            + BaseMessages.getString( PKG, "TransGraph.Dialog.HopInfo.Status" )
            + " "
            + ( hi.isEnabled() ? BaseMessages.getString( PKG, "TransGraph.Dialog.HopInfo.Enable" ) : BaseMessages
            .getString( PKG, "TransGraph.Dialog.HopInfo.Disable" ) );
        toolTip.setText( newTip );
        if ( hi.isEnabled() ) {
          toolTip.setImage( GUIResource.getInstance().getImageHop() );
        } else {
          toolTip.setImage( GUIResource.getInstance().getImageDisabledHop() );
        }
        toolTip.show( new org.eclipse.swt.graphics.Point( screenX, screenY ) );
      } else {
        newTip = null;
      }

    } else if ( !newTip.equalsIgnoreCase( getToolTipText() ) ) {
      Image tooltipImage = null;
      if ( tipImage != null ) {
        tooltipImage = tipImage;
      } else {
        tooltipImage = GUIResource.getInstance().getImageSpoonLow();
      }
      showTooltip( newTip, tooltipImage, screenX, screenY );
    }

    if ( areaOwner != null && areaOwner.getExtensionAreaType() != null ) {
      try {
        TransPainterFlyoutTooltipExtension extension =
          new TransPainterFlyoutTooltipExtension( areaOwner, this, new Point( screenX, screenY ) );

        ExtensionPointHandler.callExtensionPoint(
          LogChannel.GENERAL, KettleExtensionPoint.TransPainterFlyoutTooltip.id, extension );

      } catch ( Exception e ) {
        LogChannel.GENERAL.logError( "Error calling extension point(s) for the transformation painter step", e );
      }
    }

    return subject;
  }

  public void showTooltip( String label, Image image, int screenX, int screenY ) {
    toolTip.setImage( image );
    toolTip.setText( label );
    toolTip.hide();
    toolTip.show( new org.eclipse.swt.graphics.Point( screenX, screenY ) );
  }

  public AreaOwner getVisibleAreaOwner( int x, int y ) {
    for ( int i = areaOwners.size() - 1; i >= 0; i-- ) {
      AreaOwner areaOwner = areaOwners.get( i );
      if ( areaOwner.contains( x, y ) ) {
        return areaOwner;
      }
    }
    return null;
  }

  public void delSelected( StepMeta stMeta ) {
    List<StepMeta> selection = transMeta.getSelectedSteps();
    if ( selection.size() == 0 ) {
      spoon.delStep( transMeta, stMeta );
      return;
    }

    if ( currentStep != null && selection.contains( currentStep ) ) {
      currentStep = null;
      transPreviewDelegate.setSelectedStep( currentStep );
      transPreviewDelegate.refreshView();
      for ( StepSelectionListener listener : currentStepListeners ) {
        listener.onUpdateSelection( currentStep );
      }
    }

    StepMeta[] steps = selection.toArray( new StepMeta[ selection.size() ] );
    spoon.delSteps( transMeta, steps );
  }

  public void editDescription( StepMeta stepMeta ) {
    String title = BaseMessages.getString( PKG, "TransGraph.Dialog.StepDescription.Title" );
    String message = BaseMessages.getString( PKG, "TransGraph.Dialog.StepDescription.Message" );
    EnterTextDialog dd = new EnterTextDialog( shell, title, message, stepMeta.getDescription() );
    String d = dd.open();
    if ( d != null ) {
      stepMeta.setDescription( d );
      stepMeta.setChanged();
      spoon.setShellText();
    }
  }

  /**
   * Display the input- or outputfields for a step.
   *
   * @param stepMeta The step (it's metadata) to query
   * @param before   set to true if you want to have the fields going INTO the step, false if you want to see all the
   *                 fields that exit the step.
   */
  private void inputOutputFields( StepMeta stepMeta, boolean before ) {
    spoon.refreshGraph();

    transMeta.setRepository( spoon.rep );
    SearchFieldsProgressDialog op = new SearchFieldsProgressDialog( transMeta, stepMeta, before );
    boolean alreadyThrownError = false;
    try {
      final ProgressMonitorDialog pmd = new ProgressMonitorDialog( shell );

      // Run something in the background to cancel active database queries, forecably if needed!
      Runnable run = new Runnable() {
        @Override
        public void run() {
          IProgressMonitor monitor = pmd.getProgressMonitor();
          while ( pmd.getShell() == null || ( !pmd.getShell().isDisposed() && !monitor.isCanceled() ) ) {
            try {
              Thread.sleep( 250 );
            } catch ( InterruptedException e ) {
              // Ignore
            }
          }

          if ( monitor.isCanceled() ) { // Disconnect and see what happens!

            try {
              transMeta.cancelQueries();
            } catch ( Exception e ) {
              // Ignore
            }
          }
        }
      };
      // Dump the cancel looker in the background!
      new Thread( run ).start();

      pmd.run( true, true, op );
    } catch ( InvocationTargetException e ) {
      new ErrorDialog( shell, BaseMessages.getString( PKG, "TransGraph.Dialog.GettingFields.Title" ), BaseMessages
        .getString( PKG, "TransGraph.Dialog.GettingFields.Message" ), e );
      alreadyThrownError = true;
    } catch ( InterruptedException e ) {
      new ErrorDialog( shell, BaseMessages.getString( PKG, "TransGraph.Dialog.GettingFields.Title" ), BaseMessages
        .getString( PKG, "TransGraph.Dialog.GettingFields.Message" ), e );
      alreadyThrownError = true;
    }

    RowMetaInterface fields = op.getFields();

    if ( fields != null && fields.size() > 0 ) {
      StepFieldsDialog sfd = new StepFieldsDialog( shell, transMeta, SWT.NONE, stepMeta.getName(), fields );
      String sn = (String) sfd.open();
      if ( sn != null ) {
        StepMeta esi = transMeta.findStep( sn );
        if ( esi != null ) {
          editStep( esi );
        }
      }
    } else {
      if ( !alreadyThrownError ) {
        modalMessageDialog( getString( "TransGraph.Dialog.CouldntFindFields.Title" ),
          getString( "TransGraph.Dialog.CouldntFindFields.Message" ), SWT.OK | SWT.ICON_INFORMATION );
      }
    }

  }

  public void paintControl( PaintEvent e ) {
    Point area = getArea();
    if ( area.x == 0 || area.y == 0 ) {
      return; // nothing to do!
    }

    Display disp = shell.getDisplay();

    Image img = getTransformationImage( disp, area.x, area.y, magnification );
    e.gc.drawImage( img, 0, 0 );
    if ( transMeta.nrSteps() == 0 ) {
      e.gc.setForeground( GUIResource.getInstance().getColorCrystalTextPentaho() );
      e.gc.setFont( GUIResource.getInstance().getFontMedium() );

      Image pentahoImage = GUIResource.getInstance().getImageTransCanvas();
      int leftPosition = ( area.x - pentahoImage.getBounds().width ) / 2;
      int topPosition = ( area.y - pentahoImage.getBounds().height ) / 2;
      e.gc.drawImage( pentahoImage, leftPosition, topPosition );
    }
    img.dispose();

    // spoon.setShellText();
  }

  public Image getTransformationImage( Device device, int x, int y, float magnificationFactor ) {

    GCInterface gc = new SWTGC( device, new Point( x, y ), iconsize );

    int gridSize =
      PropsUI.getInstance().isShowCanvasGridEnabled() ? PropsUI.getInstance().getCanvasGridSize() : 1;

    TransPainter transPainter =
      new TransPainter( gc, transMeta, new Point( x, y ), new SwtScrollBar( hori ), new SwtScrollBar( vert ),
        candidate, drop_candidate, selectionRegion, areaOwners, mouseOverSteps,
        PropsUI.getInstance().getIconSize(), PropsUI.getInstance().getLineWidth(), gridSize,
        PropsUI.getInstance().getShadowSize(), PropsUI.getInstance()
        .isAntiAliasingEnabled(), PropsUI.getInstance().getNoteFont().getName(), PropsUI.getInstance()
        .getNoteFont().getHeight(), trans, PropsUI.getInstance().isIndicateSlowTransStepsEnabled() );

    transPainter.setMagnification( magnificationFactor );
    transPainter.setStepLogMap( stepLogMap );
    transPainter.setStartHopStep( startHopStep );
    transPainter.setEndHopLocation( endHopLocation );
    transPainter.setNoInputStep( noInputStep );
    transPainter.setEndHopStep( endHopStep );
    transPainter.setCandidateHopType( candidateHopType );
    transPainter.setStartErrorHopStep( startErrorHopStep );
    transPainter.setShowTargetStreamsStep( showTargetStreamsStep );

    transPainter.buildTransformationImage();

    Image img = (Image) gc.getImage();

    gc.dispose();
    return img;
  }

  @Override
  protected Point getOffset() {
    Point area = getArea();
    Point max = transMeta.getMaximum();
    Point thumb = getThumb( area, max );
    return getOffset( thumb, area );
  }

  private void editStep( StepMeta stepMeta ) {
    spoon.editStep( transMeta, stepMeta );
  }

  private void editNote( NotePadMeta ni ) {
    NotePadMeta before = (NotePadMeta) ni.clone();

    String title = BaseMessages.getString( PKG, "TransGraph.Dialog.EditNote.Title" );
    NotePadDialog dd = new NotePadDialog( transMeta, shell, title, ni );
    NotePadMeta n = dd.open();

    if ( n != null ) {
      ni.setChanged();
      ni.setNote( n.getNote() );
      ni.setFontName( n.getFontName() );
      ni.setFontSize( n.getFontSize() );
      ni.setFontBold( n.isFontBold() );
      ni.setFontItalic( n.isFontItalic() );
      // font color
      ni.setFontColorRed( n.getFontColorRed() );
      ni.setFontColorGreen( n.getFontColorGreen() );
      ni.setFontColorBlue( n.getFontColorBlue() );
      // background color
      ni.setBackGroundColorRed( n.getBackGroundColorRed() );
      ni.setBackGroundColorGreen( n.getBackGroundColorGreen() );
      ni.setBackGroundColorBlue( n.getBackGroundColorBlue() );
      // border color
      ni.setBorderColorRed( n.getBorderColorRed() );
      ni.setBorderColorGreen( n.getBorderColorGreen() );
      ni.setBorderColorBlue( n.getBorderColorBlue() );
      ni.setDrawShadow( n.isDrawShadow() );
      ni.width = ConstUI.NOTE_MIN_SIZE;
      ni.height = ConstUI.NOTE_MIN_SIZE;

      NotePadMeta after = (NotePadMeta) ni.clone();
      spoon.addUndoChange( transMeta, new NotePadMeta[] { before }, new NotePadMeta[] { after }, new int[] { transMeta
        .indexOfNote( ni ) } );
      spoon.refreshGraph();
    }
  }

  private void editHop( TransHopMeta transHopMeta ) {
    String name = transHopMeta.toString();
    if ( log.isDebug() ) {
      log.logDebug( BaseMessages.getString( PKG, "TransGraph.Logging.EditingHop" ) + name );
    }
    spoon.editHop( transMeta, transHopMeta );
  }

  private void newHop() {
    List<StepMeta> selection = transMeta.getSelectedSteps();
    if ( selection.size() == 2 ) {
      StepMeta fr = selection.get( 0 );
      StepMeta to = selection.get( 1 );
      spoon.newHop( transMeta, fr, to );
    }
  }

  private boolean pointOnLine( int x, int y, int[] line ) {
    int dx, dy;
    int pm = HOP_SEL_MARGIN / 2;
    boolean retval = false;

    for ( dx = -pm; dx <= pm && !retval; dx++ ) {
      for ( dy = -pm; dy <= pm && !retval; dy++ ) {
        retval = pointOnThinLine( x + dx, y + dy, line );
      }
    }

    return retval;
  }

  private boolean pointOnThinLine( int x, int y, int[] line ) {
    int x1 = line[ 0 ];
    int y1 = line[ 1 ];
    int x2 = line[ 2 ];
    int y2 = line[ 3 ];

    // Not in the square formed by these 2 points: ignore!
    // CHECKSTYLE:LineLength:OFF
    if ( !( ( ( x >= x1 && x <= x2 ) || ( x >= x2 && x <= x1 ) ) && ( ( y >= y1 && y <= y2 ) || ( y >= y2
      && y <= y1 ) ) ) ) {
      return false;
    }

    double angle_line = Math.atan2( y2 - y1, x2 - x1 ) + Math.PI;
    double angle_point = Math.atan2( y - y1, x - x1 ) + Math.PI;

    // Same angle, or close enough?
    if ( angle_point >= angle_line - 0.01 && angle_point <= angle_line + 0.01 ) {
      return true;
    }

    return false;
  }

  private SnapAllignDistribute createSnapAllignDistribute() {
    List<StepMeta> selection = transMeta.getSelectedSteps();
    int[] indices = transMeta.getStepIndexes( selection );

    return new SnapAllignDistribute( transMeta, selection, indices, spoon, this );
  }

  public void snaptogrid() {
    snaptogrid( ConstUI.GRID_SIZE );
  }

  private void snaptogrid( int size ) {
    createSnapAllignDistribute().snaptogrid( size );
  }

  public void allignleft() {
    createSnapAllignDistribute().allignleft();
  }

  public void allignright() {
    createSnapAllignDistribute().allignright();
  }

  public void alligntop() {
    createSnapAllignDistribute().alligntop();
  }

  public void allignbottom() {
    createSnapAllignDistribute().allignbottom();
  }

  public void distributehorizontal() {
    createSnapAllignDistribute().distributehorizontal();
  }

  public void distributevertical() {
    createSnapAllignDistribute().distributevertical();
  }

  private void detach( StepMeta stepMeta ) {

    for ( int i = transMeta.nrTransHops() - 1; i >= 0; i-- ) {
      TransHopMeta hop = transMeta.getTransHop( i );
      if ( stepMeta.equals( hop.getFromStep() ) || stepMeta.equals( hop.getToStep() ) ) {
        // Step is connected with a hop, remove this hop.
        //
        spoon.addUndoNew( transMeta, new TransHopMeta[] { hop }, new int[] { i } );
        transMeta.removeTransHop( i );
      }
    }

    /*
     * TransHopMeta hfrom = transMeta.findTransHopTo(stepMeta); TransHopMeta hto = transMeta.findTransHopFrom(stepMeta);
     *
     * if (hfrom != null && hto != null) { if (transMeta.findTransHop(hfrom.getFromStep(), hto.getToStep()) == null) {
     * TransHopMeta hnew = new TransHopMeta(hfrom.getFromStep(), hto.getToStep()); transMeta.addTransHop(hnew);
     * spoon.addUndoNew(transMeta, new TransHopMeta[] { hnew }, new int[] { transMeta.indexOfTransHop(hnew) });
     * spoon.refreshTree(); } } if (hfrom != null) { int fromidx = transMeta.indexOfTransHop(hfrom); if (fromidx >= 0) {
     * transMeta.removeTransHop(fromidx); spoon.refreshTree(); } } if (hto != null) { int toidx =
     * transMeta.indexOfTransHop(hto); if (toidx >= 0) { transMeta.removeTransHop(toidx); spoon.refreshTree(); } }
     */
    spoon.refreshTree();
    redraw();
  }

  // Preview the selected steps...
  public void preview() {
    spoon.previewTransformation();
  }

  public void newProps() {
    iconsize = spoon.props.getIconSize();
  }

  @Override
  public EngineMetaInterface getMeta() {
    return transMeta;
  }

  /**
   * @param transMeta the transMeta to set
   * @return the transMeta / public TransMeta getTransMeta() { return transMeta; }
   * <p/>
   * /**
   */
  public void setTransMeta( TransMeta transMeta ) {
    this.transMeta = transMeta;
  }

  // Change of step, connection, hop or note...
  public void addUndoPosition( Object[] obj, int[] pos, Point[] prev, Point[] curr ) {
    addUndoPosition( obj, pos, prev, curr, false );
  }

  // Change of step, connection, hop or note...
  public void addUndoPosition( Object[] obj, int[] pos, Point[] prev, Point[] curr, boolean nextAlso ) {
    // It's better to store the indexes of the objects, not the objects itself!
    transMeta.addUndo( obj, null, pos, prev, curr, TransMeta.TYPE_UNDO_POSITION, nextAlso );
    spoon.setUndoMenu( transMeta );
  }

  @Override
  public boolean applyChanges() throws KettleException {
    return spoon.saveToFile( transMeta );
  }

  @Override
  public boolean canBeClosed() {
    return !transMeta.hasChanged();
  }

  @Override
  public TransMeta getManagedObject() {
    return transMeta;
  }

  @Override
  public boolean hasContentChanged() {
    return transMeta.hasChanged();
  }

  public List<CheckResultInterface> getRemarks() {
    return remarks;
  }

  public void setRemarks( List<CheckResultInterface> remarks ) {
    this.remarks = remarks;
  }

  public List<DatabaseImpact> getImpact() {
    return impact;
  }

  public void setImpact( List<DatabaseImpact> impact ) {
    this.impact = impact;
  }

  public boolean isImpactFinished() {
    return impactFinished;
  }

  public void setImpactFinished( boolean impactHasRun ) {
    this.impactFinished = impactHasRun;
  }

  /**
   * @return the lastMove
   */
  public Point getLastMove() {
    return lastMove;
  }

  public static boolean editProperties( TransMeta transMeta, Spoon spoon, Repository rep,
                                        boolean allowDirectoryChange ) {
    return editProperties( transMeta, spoon, rep, allowDirectoryChange, null );

  }

  public static boolean editProperties( TransMeta transMeta, Spoon spoon, Repository rep, boolean allowDirectoryChange,
                                        TransDialog.Tabs currentTab ) {
    if ( transMeta == null ) {
      return false;
    }

    TransDialog tid = new TransDialog( spoon.getShell(), SWT.NONE, transMeta, rep, currentTab );
    tid.setDirectoryChangeAllowed( allowDirectoryChange );
    TransMeta ti = tid.open();

    // Load shared objects
    //
    if ( tid.isSharedObjectsFileChanged() ) {
      try {
        SharedObjects sharedObjects =
          rep != null ? rep.readTransSharedObjects( transMeta ) : transMeta.readSharedObjects();
        spoon.sharedObjectsFileMap.put( sharedObjects.getFilename(), sharedObjects );
      } catch ( KettleException e ) {
        // CHECKSTYLE:LineLength:OFF
        new ErrorDialog( spoon.getShell(),
          BaseMessages.getString( PKG, "Spoon.Dialog.ErrorReadingSharedObjects.Title" ), BaseMessages.getString( PKG,
          "Spoon.Dialog.ErrorReadingSharedObjects.Message", spoon.makeTabName( transMeta, true ) ), e );
      }

      // If we added properties, add them to the variables too, so that they appear in the CTRL-SPACE variable
      // completion.
      //
      spoon.setParametersAsVariablesInUI( transMeta, transMeta );

      spoon.refreshTree();
      spoon.delegates.tabs.renameTabs(); // cheap operation, might as will do it anyway
    }

    spoon.setShellText();
    return ti != null;
  }

  public void openFile() {
    spoon.openFile();
  }

  public void saveFile() throws KettleException {
    spoon.saveFile();
  }

  public void saveFileAs() throws KettleException {
    spoon.saveFileAs();
  }

  public void saveXMLFileToVfs() {
    spoon.saveXMLFileToVfs();
  }

  public void printFile() {
    spoon.printFile();
  }

  public void runTransformation() {
    spoon.runFile();
  }

  public void runOptionsTransformation() {
    spoon.runOptionsFile();
  }

  public void pauseTransformation() {
    pauseResume();
  }

  public void stopTransformation() {
    stop();
  }

  public void previewFile() {
    spoon.previewFile();
  }

  public void debugFile() {
    spoon.debugFile();
  }

  public void transReplay() {
    spoon.replayTransformation();
  }

  public void checkTrans() {
    spoon.checkTrans();
  }

  public void analyseImpact() {
    spoon.analyseImpact();
  }

  public void getSQL() {
    spoon.getSQL();
  }

  public void exploreDatabase() {
    spoon.exploreDatabase();
  }

  public boolean isExecutionResultsPaneVisible() {
    return extraViewComposite != null && !extraViewComposite.isDisposed();
  }

  public void showExecutionResults() {

    if ( isExecutionResultsPaneVisible() ) {
      disposeExtraView();
    } else {
      addAllTabs();
    }
  }

  public void browseVersionHistory() {
    try {
      if ( spoon.rep.exists( transMeta.getName(), transMeta.getRepositoryDirectory(),
        RepositoryObjectType.TRANSFORMATION ) ) {
        RepositoryRevisionBrowserDialogInterface dialog =
          RepositoryExplorerDialog.getVersionBrowserDialog( shell, spoon.rep, transMeta );
        String versionLabel = dialog.open();
        if ( versionLabel != null ) {
          spoon.loadObjectFromRepository( transMeta.getName(), transMeta.getRepositoryElementType(), transMeta
            .getRepositoryDirectory(), versionLabel );
        }
      } else {
        modalMessageDialog( getString( "TransGraph.Sorry" ),
          getString( "TransGraph.VersionBrowser.CantFindInRepo" ), SWT.CLOSE | SWT.ICON_ERROR );
      }
    } catch ( Exception e ) {
      new ErrorDialog( shell, BaseMessages.getString( PKG, "TransGraph.VersionBrowserException.Title" ), BaseMessages
        .getString( PKG, "TransGraph.VersionBrowserException.Message" ), e );
    }
  }

  /**
   * If the extra tab view at the bottom is empty, we close it.
   */
  public void checkEmptyExtraView() {
    if ( extraViewTabFolder.getItemCount() == 0 ) {
      disposeExtraView();
    }
  }

  private void disposeExtraView() {

    extraViewComposite.dispose();
    sashForm.layout();
    sashForm.setWeights( new int[] { 100, } );

    XulToolbarbutton button = (XulToolbarbutton) toolbar.getElementById( "trans-show-results" );
    button.setTooltiptext( BaseMessages.getString( PKG, "Spoon.Tooltip.ShowExecutionResults" ) );
    ToolItem toolItem = (ToolItem) button.getManagedObject();
    toolItem.setImage( GUIResource.getInstance().getImageShowResults() );
  }

  private void minMaxExtraView() {
    // What is the state?
    //
    boolean maximized = sashForm.getMaximizedControl() != null;
    if ( maximized ) {
      // Minimize
      //
      sashForm.setMaximizedControl( null );
      minMaxButton.setImage( GUIResource.getInstance().getImageMaximizePanel() );
      minMaxButton
        .setToolTipText( BaseMessages.getString( PKG, "TransGraph.ExecutionResultsPanel.MaxButton.Tooltip" ) );
    } else {
      // Maximize
      //
      sashForm.setMaximizedControl( extraViewComposite );
      minMaxButton.setImage( GUIResource.getInstance().getImageMinimizePanel() );
      minMaxButton
        .setToolTipText( BaseMessages.getString( PKG, "TransGraph.ExecutionResultsPanel.MinButton.Tooltip" ) );
    }
  }

  /**
   * @return the toolbar
   */
  public XulToolbar getToolbar() {
    return toolbar;
  }

  /**
   * @param toolbar the toolbar to set
   */
  public void setToolbar( XulToolbar toolbar ) {
    this.toolbar = toolbar;
  }

  private Label closeButton;

  private Label minMaxButton;

  /**
   * Add an extra view to the main composite SashForm
   */
  public void addExtraView() {
    extraViewComposite = new Composite( sashForm, SWT.NONE );
    FormLayout extraCompositeFormLayout = new FormLayout();
    extraCompositeFormLayout.marginWidth = 2;
    extraCompositeFormLayout.marginHeight = 2;
    extraViewComposite.setLayout( extraCompositeFormLayout );

    // Put a close and max button to the upper right corner...
    //
    closeButton = new Label( extraViewComposite, SWT.NONE );
    closeButton.setImage( GUIResource.getInstance().getImageClosePanel() );
    closeButton.setToolTipText( BaseMessages.getString( PKG, "TransGraph.ExecutionResultsPanel.CloseButton.Tooltip" ) );
    FormData fdClose = new FormData();
    fdClose.right = new FormAttachment( 100, 0 );
    fdClose.top = new FormAttachment( 0, 0 );
    closeButton.setLayoutData( fdClose );
    closeButton.addMouseListener( new MouseAdapter() {
      @Override
      public void mouseDown( MouseEvent e ) {
        disposeExtraView();
      }
    } );

    minMaxButton = new Label( extraViewComposite, SWT.NONE );
    minMaxButton.setImage( GUIResource.getInstance().getImageMaximizePanel() );
    minMaxButton.setToolTipText( BaseMessages.getString( PKG, "TransGraph.ExecutionResultsPanel.MaxButton.Tooltip" ) );
    FormData fdMinMax = new FormData();
    fdMinMax.right = new FormAttachment( closeButton, -Const.MARGIN );
    fdMinMax.top = new FormAttachment( 0, 0 );
    minMaxButton.setLayoutData( fdMinMax );
    minMaxButton.addMouseListener( new MouseAdapter() {
      @Override
      public void mouseDown( MouseEvent e ) {
        minMaxExtraView();
      }
    } );

    // Add a label at the top: Results
    //
    Label wResultsLabel = new Label( extraViewComposite, SWT.LEFT );
    wResultsLabel.setFont( GUIResource.getInstance().getFontMediumBold() );
    wResultsLabel.setBackground( GUIResource.getInstance().getColorWhite() );
    wResultsLabel.setText( BaseMessages.getString( PKG, "TransLog.ResultsPanel.NameLabel" ) );
    FormData fdResultsLabel = new FormData();
    fdResultsLabel.left = new FormAttachment( 0, 0 );
    fdResultsLabel.right = new FormAttachment( minMaxButton, -Const.MARGIN );
    fdResultsLabel.top = new FormAttachment( 0, 0 );
    wResultsLabel.setLayoutData( fdResultsLabel );

    // Add a tab folder ...
    //
    extraViewTabFolder = new CTabFolder( extraViewComposite, SWT.MULTI );
    spoon.props.setLook( extraViewTabFolder, Props.WIDGET_STYLE_TAB );

    extraViewTabFolder.addMouseListener( new MouseAdapter() {

      @Override
      public void mouseDoubleClick( MouseEvent arg0 ) {
        if ( sashForm.getMaximizedControl() == null ) {
          sashForm.setMaximizedControl( extraViewComposite );
        } else {
          sashForm.setMaximizedControl( null );
        }
      }

    } );

    FormData fdTabFolder = new FormData();
    fdTabFolder.left = new FormAttachment( 0, 0 );
    fdTabFolder.right = new FormAttachment( 100, 0 );
    fdTabFolder.top = new FormAttachment( wResultsLabel, Const.MARGIN );
    fdTabFolder.bottom = new FormAttachment( 100, 0 );
    extraViewTabFolder.setLayoutData( fdTabFolder );

    sashForm.setWeights( new int[] { 60, 40, } );
  }

  /**
   * @deprecated Deprecated as of 8.0. Seems unused; will be to remove in 8.1 (ccaspanello)
   */
  @Deprecated
  public void checkErrors() {
    if ( trans != null ) {
      if ( !trans.isFinished() ) {
        if ( trans.getErrors() != 0 ) {
          trans.killAll();
        }
      }
    }
  }

  public synchronized void start( TransExecutionConfiguration executionConfiguration ) throws KettleException {
    // Auto save feature...
    handleTransMetaChanges( transMeta );

    if ( ( ( transMeta.getName() != null && transMeta.getObjectId() != null && spoon.rep != null ) || // Repository
      // available &
      // name / id set
      ( transMeta.getFilename() != null && spoon.rep == null ) // No repository & filename set
    )
      && !transMeta.hasChanged() // Didn't change
      ) {
      if ( trans == null || !running ) {
        try {
          // Set the requested logging level..
          //
          DefaultLogLevel.setLogLevel( executionConfiguration.getLogLevel() );

          transMeta.injectVariables( executionConfiguration.getVariables() );

          // Set the named parameters
          Map<String, String> paramMap = executionConfiguration.getParams();
          Set<String> keys = paramMap.keySet();
          for ( String key : keys ) {
            transMeta.setParameterValue( key, Const.NVL( paramMap.get( key ), "" ) );
          }

          transMeta.activateParameters();

          // Do we need to clear the log before running?
          //
          if ( executionConfiguration.isClearingLog() ) {
            transLogDelegate.clearLog();
          }

          // Also make sure to clear the log entries in the central log store & registry
          //
          if ( trans != null ) {
            KettleLogStore.discardLines( trans.getLogChannelId(), true );
          }

          // Important: even though transMeta is passed to the Trans constructor, it is not the same object as is in
          // memory
          // To be able to completely test this, we need to run it as we would normally do in pan
          //
          trans = new TransSupplier( transMeta, log, this::createLegacyTrans ).get();

          trans.setRepository( spoon.getRepository() );
          trans.setMetaStore( spoon.getMetaStore() );

          String spoonLogObjectId = UUID.randomUUID().toString();
          SimpleLoggingObject spoonLoggingObject = new SimpleLoggingObject( "SPOON", LoggingObjectType.SPOON, null );
          spoonLoggingObject.setContainerObjectId( spoonLogObjectId );
          spoonLoggingObject.setLogLevel( executionConfiguration.getLogLevel() );
          trans.setParent( spoonLoggingObject );

          trans.setLogLevel( executionConfiguration.getLogLevel() );
          trans.setReplayDate( executionConfiguration.getReplayDate() );
          trans.setRepository( executionConfiguration.getRepository() );
          trans.setMonitored( true );
          log.logBasic( BaseMessages.getString( PKG, "TransLog.Log.TransformationOpened" ) );
        } catch ( KettleException e ) {
          trans = null;
          new ErrorDialog( shell, BaseMessages.getString( PKG, "TransLog.Dialog.ErrorOpeningTransformation.Title" ),
            BaseMessages.getString( PKG, "TransLog.Dialog.ErrorOpeningTransformation.Message" ), e );
        }
        if ( trans != null ) {
          Map<String, String> arguments = executionConfiguration.getArguments();
          final String[] args;
          if ( arguments != null ) {
            args = convertArguments( arguments );
          } else {
            args = null;
          }

          log.logMinimal( BaseMessages.getString( PKG, "TransLog.Log.LaunchingTransformation" )
            + trans.getTransMeta().getName() + "]..." );

          trans.setSafeModeEnabled( executionConfiguration.isSafeModeEnabled() );
          trans.setGatheringMetrics( executionConfiguration.isGatheringMetrics() );

          // Launch the step preparation in a different thread.
          // That way Spoon doesn't block anymore and that way we can follow the progress of the initialization
          //
          final Thread parentThread = Thread.currentThread();

          shell.getDisplay().asyncExec( new Runnable() {
            @Override
            public void run() {
              addAllTabs();
              prepareTrans( parentThread, args );
            }
          } );

          log.logMinimal( BaseMessages.getString( PKG, "TransLog.Log.StartedExecutionOfTransformation" ) );

          setControlStates();
        }
      } else {
        modalMessageDialog( getString( "TransLog.Dialog.DoNoStartTransformationTwice.Title" ),
          getString( "TransLog.Dialog.DoNoStartTransformationTwice.Message" ), SWT.OK | SWT.ICON_WARNING );
      }
    } else {
      if ( transMeta.hasChanged() ) {
        showSaveFileMessage();
      } else if ( spoon.rep != null && transMeta.getName() == null ) {
        modalMessageDialog( getString( "TransLog.Dialog.GiveTransformationANameBeforeRunning.Title" ),
          getString( "TransLog.Dialog.GiveTransformationANameBeforeRunning.Message" ), SWT.OK | SWT.ICON_WARNING );
      } else {
        modalMessageDialog( getString( "TransLog.Dialog.SaveTransformationBeforeRunning2.Title" ),
          getString( "TransLog.Dialog.SaveTransformationBeforeRunning2.Message" ), SWT.OK | SWT.ICON_WARNING );
      }
    }
  }

  public void showSaveFileMessage() {
    modalMessageDialog( getString( "TransLog.Dialog.SaveTransformationBeforeRunning.Title" ),
      getString( "TransLog.Dialog.SaveTransformationBeforeRunning.Message" ), SWT.OK | SWT.ICON_WARNING );
  }

  public void addAllTabs() {

    CTabItem tabItemSelection = null;
    if ( extraViewTabFolder != null && !extraViewTabFolder.isDisposed() ) {
      tabItemSelection = extraViewTabFolder.getSelection();
    }

    transHistoryDelegate.addTransHistory();
    transLogDelegate.addTransLog();
    transGridDelegate.addTransGrid();
    transPerfDelegate.addTransPerf();
    transMetricsDelegate.addTransMetrics();
    transPreviewDelegate.addTransPreview();

    List<SpoonUiExtenderPluginInterface> relevantExtenders =
      SpoonUiExtenderPluginType.getInstance().getRelevantExtenders( TransGraph.class, LOAD_TAB );

    for ( SpoonUiExtenderPluginInterface relevantExtender : relevantExtenders ) {
      relevantExtender.uiEvent( this, LOAD_TAB );
    }

    if ( tabItemSelection != null ) {
      extraViewTabFolder.setSelection( tabItemSelection );
    } else {
      extraViewTabFolder.setSelection( transGridDelegate.getTransGridTab() );
    }

    XulToolbarbutton button = (XulToolbarbutton) toolbar.getElementById( "trans-show-results" );
    button.setTooltiptext( BaseMessages.getString( PKG, "Spoon.Tooltip.HideExecutionResults" ) );
    ToolItem toolItem = (ToolItem) button.getManagedObject();
    toolItem.setImage( GUIResource.getInstance().getImageHideResults() );
  }

  public synchronized void debug( TransExecutionConfiguration executionConfiguration, TransDebugMeta transDebugMeta ) {
    if ( !running ) {
      try {
        this.lastTransDebugMeta = transDebugMeta;

        log.setLogLevel( executionConfiguration.getLogLevel() );
        if ( log.isDetailed() ) {
          log.logDetailed( BaseMessages.getString( PKG, "TransLog.Log.DoPreview" ) );
        }
        String[] args = null;
        Map<String, String> arguments = executionConfiguration.getArguments();
        if ( arguments != null ) {
          args = convertArguments( arguments );
        }
        transMeta.injectVariables( executionConfiguration.getVariables() );

        // Set the named parameters
        Map<String, String> paramMap = executionConfiguration.getParams();
        Set<String> keys = paramMap.keySet();
        for ( String key : keys ) {
          transMeta.setParameterValue( key, Const.NVL( paramMap.get( key ), "" ) );
        }

        transMeta.activateParameters();

        // Do we need to clear the log before running?
        //
        if ( executionConfiguration.isClearingLog() ) {
          transLogDelegate.clearLog();
        }

        // Do we have a previous execution to clean up in the logging registry?
        //
        if ( trans != null ) {
          KettleLogStore.discardLines( trans.getLogChannelId(), false );
          LoggingRegistry.getInstance().removeIncludingChildren( trans.getLogChannelId() );
        }

        // Create a new transformation to execution
        //
        trans = new Trans( transMeta );
        trans.setSafeModeEnabled( executionConfiguration.isSafeModeEnabled() );
        trans.setPreview( true );
        trans.setGatheringMetrics( executionConfiguration.isGatheringMetrics() );
        trans.setMetaStore( spoon.getMetaStore() );
        trans.prepareExecution( args );
        trans.setRepository( spoon.rep );

        List<SpoonUiExtenderPluginInterface> relevantExtenders =
          SpoonUiExtenderPluginType.getInstance().getRelevantExtenders( TransDebugMetaWrapper.class, PREVIEW_TRANS );
        TransDebugMetaWrapper transDebugMetaWrapper = new TransDebugMetaWrapper( trans, transDebugMeta );
        for ( SpoonUiExtenderPluginInterface relevantExtender : relevantExtenders ) {
          relevantExtender.uiEvent( transDebugMetaWrapper, PREVIEW_TRANS );
        }
        // Add the row listeners to the allocated threads
        //
        transDebugMeta.addRowListenersToTransformation( trans );

        // What method should we call back when a break-point is hit?

        transDebugMeta.addBreakPointListers( new BreakPointListener() {
          @Override
          public void breakPointHit( TransDebugMeta transDebugMeta, StepDebugMeta stepDebugMeta,
                                     RowMetaInterface rowBufferMeta, List<Object[]> rowBuffer ) {
            showPreview( transDebugMeta, stepDebugMeta, rowBufferMeta, rowBuffer );
          }
        } );

        // Do we capture data?
        //
        if ( transPreviewDelegate.isActive() ) {
          transPreviewDelegate.capturePreviewData( trans, transMeta.getSteps() );
        }

        // Start the threads for the steps...
        //
        startThreads();

        debug = true;

        // Show the execution results view...
        //
        shell.getDisplay().asyncExec( new Runnable() {
          @Override
          public void run() {
            addAllTabs();
          }
        } );
      } catch ( Exception e ) {
        new ErrorDialog( shell, BaseMessages.getString( PKG, "TransLog.Dialog.UnexpectedErrorDuringPreview.Title" ),
          BaseMessages.getString( PKG, "TransLog.Dialog.UnexpectedErrorDuringPreview.Message" ), e );
      }
    } else {
      modalMessageDialog( getString( "TransLog.Dialog.DoNoPreviewWhileRunning.Title" ),
        getString( "TransLog.Dialog.DoNoPreviewWhileRunning.Message" ), SWT.OK | SWT.ICON_WARNING );
    }
    checkErrorVisuals();
  }

  public synchronized void showPreview( final TransDebugMeta transDebugMeta, final StepDebugMeta stepDebugMeta,
                                        final RowMetaInterface rowBufferMeta, final List<Object[]> rowBuffer ) {
    shell.getDisplay().asyncExec( new Runnable() {

      @Override
      public void run() {

        if ( isDisposed() ) {
          return;
        }

        spoon.enableMenus();

        // The transformation is now paused, indicate this in the log dialog...
        //
        pausing = true;

        setControlStates();
        checkErrorVisuals();

        PreviewRowsDialog previewRowsDialog =
          new PreviewRowsDialog(
            shell, transMeta, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | SWT.APPLICATION_MODAL | SWT.SHEET,
            stepDebugMeta.getStepMeta().getName(), rowBufferMeta, rowBuffer );
        previewRowsDialog.setProposingToGetMoreRows( true );
        previewRowsDialog.setProposingToStop( true );
        previewRowsDialog.open();

        if ( previewRowsDialog.isAskingForMoreRows() ) {
          // clear the row buffer.
          // That way if you click resume, you get the next N rows for the step :-)
          //
          rowBuffer.clear();

          // Resume running: find more rows...
          //
          pauseResume();
        }

        if ( previewRowsDialog.isAskingToStop() ) {
          // Stop running
          //
          stop();
        }
      }
    } );
  }

  private String[] convertArguments( Map<String, String> arguments ) {
    String[] argumentNames = arguments.keySet().toArray( new String[ arguments.size() ] );
    Arrays.sort( argumentNames );

    String[] args = new String[ argumentNames.length ];
    for ( int i = 0; i < args.length; i++ ) {
      String argumentName = argumentNames[ i ];
      args[ i ] = arguments.get( argumentName );
    }
    return args;
  }

  public void stop() {
    if ( safeStopping ) {
      modalMessageDialog( getString( "TransLog.Log.SafeStopAlreadyStarted.Title" ),
        getString( "TransLog.Log.SafeStopAlreadyStarted" ), SWT.ICON_ERROR | SWT.OK );
      return;
    }
    if ( ( running && !halting ) ) {
      halting = true;
      trans.stopAll();
      log.logMinimal( BaseMessages.getString( PKG, "TransLog.Log.ProcessingOfTransformationStopped" ) );

      running = false;
      initialized = false;
      halted = false;
      halting = false;

      setControlStates();

      transMeta.setInternalKettleVariables(); // set the original vars back as they may be changed by a mapping
    }
  }

  public void safeStop() {
    if ( running && !halting ) {
      halting = true;
      safeStopping = true;
      trans.safeStop();
      log.logMinimal( BaseMessages.getString( PKG, "TransLog.Log.TransformationSafeStopped" ) );

      initialized = false;
      halted = false;

      setControlStates();

      transMeta.setInternalKettleVariables(); // set the original vars back as they may be changed by a mapping
    }
  }

  public synchronized void pauseResume() {
    if ( running ) {
      // Get the pause toolbar item
      //
      if ( !pausing ) {
        pausing = true;
        trans.pauseRunning();
        setControlStates();
      } else {
        pausing = false;
        trans.resumeRunning();
        setControlStates();
      }
    }
  }

  private boolean controlDisposed( XulToolbarbutton button ) {
    if ( button.getManagedObject() instanceof Widget ) {
      Widget widget = (Widget) button.getManagedObject();
      return widget.isDisposed();
    }
    return false;
  }

  @Override
  public synchronized void setControlStates() {
    if ( isDisposed() || getDisplay().isDisposed() ) {
      return;
    }
    if ( ( (Control) toolbar.getManagedObject() ).isDisposed() ) {
      return;
    }

    getDisplay().asyncExec( new Runnable() {

      @Override
      public void run() {
        boolean operationsNotAllowed = false;
        try {
          operationsNotAllowed =
            RepositorySecurityUI.verifyOperations( shell, spoon.rep, false,
              RepositoryOperation.EXECUTE_TRANSFORMATION );
        } catch ( KettleRepositoryLostException krle ) {
          log.logError( krle.getLocalizedMessage() );
          spoon.handleRepositoryLost( krle );
        }

        // Start/Run button...
        //
        XulToolbarbutton runButton = (XulToolbarbutton) toolbar.getElementById( "trans-run" );
        if ( runButton != null && !controlDisposed( runButton ) && !operationsNotAllowed ) {
          if ( runButton.isDisabled() ^ running ) {
            runButton.setDisabled( running );
          }
        }

        // Pause button...
        //
        XulToolbarbutton pauseButton = (XulToolbarbutton) toolbar.getElementById( "trans-pause" );
        if ( pauseButton != null && !controlDisposed( pauseButton ) ) {
          if ( pauseButton.isDisabled() ^ !running ) {
            pauseButton.setDisabled( !running );
            pauseButton.setLabel( pausing ? RESUME_TEXT : PAUSE_TEXT );
            pauseButton.setTooltiptext( pausing ? BaseMessages.getString( PKG, "Spoon.Tooltip.ResumeTranformation" )
              : BaseMessages.getString( PKG, "Spoon.Tooltip.PauseTranformation" ) );
          }
        }

        // Stop button...
        //
        if ( !stopItem.isDisposed() && !stopItem.isEnabled() ^ !running ) {
          stopItem.setEnabled( running );
        }

        // Debug button...
        //
        XulToolbarbutton debugButton = (XulToolbarbutton) toolbar.getElementById( "trans-debug" );

        if ( debugButton != null && !controlDisposed( debugButton ) && !operationsNotAllowed ) {
          if ( debugButton.isDisabled() ^ running ) {
            debugButton.setDisabled( running );
          }
        }

        // Preview button...
        //
        XulToolbarbutton previewButton = (XulToolbarbutton) toolbar.getElementById( "trans-preview" );
        if ( previewButton != null && !controlDisposed( previewButton ) && !operationsNotAllowed ) {
          if ( previewButton.isDisabled() ^ running ) {
            previewButton.setDisabled( running );
          }
        }
      }

    } );

  }

  private synchronized void prepareTrans( final Thread parentThread, final String[] args ) {
    Runnable runnable = new Runnable() {
      @Override
      public void run() {
        try {
          trans.prepareExecution( args );

          // Do we capture data?
          //
          if ( transPreviewDelegate.isActive() ) {
            transPreviewDelegate.capturePreviewData( trans, transMeta.getSteps() );
          }

          initialized = true;
        } catch ( KettleException e ) {
          log.logError( trans.getName() + ": preparing transformation execution failed", e );
          checkErrorVisuals();
        }
        halted = trans.hasHaltedSteps();
        if ( trans.isReadyToStart() ) {
          checkStartThreads(); // After init, launch the threads.
        } else {
          initialized = false;
          running = false;
          checkErrorVisuals();
        }
      }
    };
    Thread thread = new Thread( runnable );
    thread.start();
  }

  private void checkStartThreads() {
    if ( initialized && !running && trans != null ) {
      startThreads();
    }
  }

  private synchronized void startThreads() {
    running = true;
    try {
      // Add a listener to the transformation.
      // If the transformation is done, we want to do the end processing, etc.
      //
      trans.addTransListener( new org.pentaho.di.trans.TransAdapter() {

        @Override
        public void transFinished( Trans trans ) {
          checkTransEnded();
          checkErrorVisuals();
          stopRedrawTimer();

          transMetricsDelegate.resetLastRefreshTime();
          transMetricsDelegate.updateGraph();
        }
      } );

      trans.startThreads();
      startRedrawTimer();

      setControlStates();
    } catch ( KettleException e ) {
      log.logError( "Error starting step threads", e );
      checkErrorVisuals();
      stopRedrawTimer();
    }

    // See if we have to fire off the performance graph updater etc.
    //
    getDisplay().asyncExec( new Runnable() {
      @Override
      public void run() {
        if ( transPerfDelegate.getTransPerfTab() != null ) {
          // If there is a tab open, try to the correct content on there now
          //
          transPerfDelegate.setupContent();
          transPerfDelegate.layoutPerfComposite();
        }
      }
    } );
  }

  private void startRedrawTimer() {

    redrawTimer = new Timer( "TransGraph: redraw timer" );
    TimerTask timtask = new TimerTask() {
      @Override
      public void run() {
        if ( !spoon.getDisplay().isDisposed() ) {
          spoon.getDisplay().asyncExec( new Runnable() {
            @Override
            public void run() {
              if ( !TransGraph.this.canvas.isDisposed() ) {
                TransGraph.this.canvas.redraw();
              }
            }
          } );
        }
      }
    };

    redrawTimer.schedule( timtask, 0L, ConstUI.INTERVAL_MS_TRANS_CANVAS_REFRESH );

  }

  protected void stopRedrawTimer() {
    if ( redrawTimer != null ) {
      redrawTimer.cancel();
      redrawTimer.purge();
      redrawTimer = null;
    }

  }

  private void checkTransEnded() {
    if ( trans != null ) {
      if ( trans.isFinished() && ( running || halted ) ) {
        log.logMinimal( BaseMessages.getString( PKG, "TransLog.Log.TransformationHasFinished" ) );

        running = false;
        initialized = false;
        halted = false;
        halting = false;
        safeStopping = false;

        setControlStates();

        // OK, also see if we had a debugging session going on.
        // If so and we didn't hit a breakpoint yet, display the show
        // preview dialog...
        //
        if ( debug && lastTransDebugMeta != null && lastTransDebugMeta.getTotalNumberOfHits() == 0 ) {
          debug = false;
          showLastPreviewResults();
        }
        debug = false;

        checkErrorVisuals();

        shell.getDisplay().asyncExec( new Runnable() {
          @Override
          public void run() {
            spoon.fireMenuControlers();
            redraw();
          }
        } );
      }
    }
  }

  private void checkErrorVisuals() {
    if ( trans.getErrors() > 0 ) {
      // Get the logging text and filter it out. Store it in the stepLogMap...
      //
      stepLogMap = new HashMap<>();
      shell.getDisplay().syncExec( new Runnable() {

        @Override
        public void run() {

          for ( StepMetaDataCombi combi : trans.getSteps() ) {
            if ( combi.step.getErrors() > 0 ) {
              String channelId = combi.step.getLogChannel().getLogChannelId();
              List<KettleLoggingEvent> eventList =
                KettleLogStore.getLogBufferFromTo( channelId, false, 0, KettleLogStore.getLastBufferLineNr() );
              StringBuilder logText = new StringBuilder();
              for ( KettleLoggingEvent event : eventList ) {
                Object message = event.getMessage();
                if ( message instanceof LogMessage ) {
                  LogMessage logMessage = (LogMessage) message;
                  if ( logMessage.isError() ) {
                    logText.append( logMessage.getMessage() ).append( Const.CR );
                  }
                }
              }
              stepLogMap.put( combi.stepMeta, logText.toString() );
            }
          }
        }
      } );

    } else {
      stepLogMap = null;
    }
    // Redraw the canvas to show the error icons etc.
    //
    shell.getDisplay().asyncExec( new Runnable() {
      @Override
      public void run() {
        redraw();
      }
    } );
  }

  public synchronized void showLastPreviewResults() {
    if ( lastTransDebugMeta == null || lastTransDebugMeta.getStepDebugMetaMap().isEmpty() ) {
      return;
    }

    final List<String> stepnames = new ArrayList<>();
    final List<RowMetaInterface> rowMetas = new ArrayList<>();
    final List<List<Object[]>> rowBuffers = new ArrayList<>();

    // Assemble the buffers etc in the old style...
    //
    for ( StepMeta stepMeta : lastTransDebugMeta.getStepDebugMetaMap().keySet() ) {
      StepDebugMeta stepDebugMeta = lastTransDebugMeta.getStepDebugMetaMap().get( stepMeta );

      stepnames.add( stepMeta.getName() );
      rowMetas.add( stepDebugMeta.getRowBufferMeta() );
      rowBuffers.add( stepDebugMeta.getRowBuffer() );
    }

    getDisplay().asyncExec( new Runnable() {
      @Override
      public void run() {
        EnterPreviewRowsDialog dialog = new EnterPreviewRowsDialog( shell, SWT.NONE, stepnames, rowMetas, rowBuffers );
        dialog.open();
      }
    } );
  }

  /**
   * XUL dummy menu entry
   */
  public void openMapping() {
  }

  /**
   * Open the transformation mentioned in the mapping...
   */
  public void openMapping( StepMeta stepMeta, int index ) {
    try {
      Object referencedMeta = null;
      Trans subTrans = getActiveSubtransformation( this, stepMeta );
      if ( subTrans != null
        && ( stepMeta.getStepMetaInterface().getActiveReferencedObjectDescription() == null || index < 0 ) ) {
        TransMeta subTransMeta = subTrans.getTransMeta();
        referencedMeta = subTransMeta;
        Object[] objectArray = new Object[ 4 ];
        objectArray[ 0 ] = stepMeta;
        objectArray[ 1 ] = subTransMeta;
        ExtensionPointHandler.callExtensionPoint( log, KettleExtensionPoint.OpenMapping.id, objectArray );
      } else {
        StepMetaInterface meta = stepMeta.getStepMetaInterface();
        if ( !Utils.isEmpty( meta.getReferencedObjectDescriptions() ) ) {
          referencedMeta = meta.loadReferencedObject( index, spoon.rep, spoon.metaStore, transMeta );
        }
      }
      if ( referencedMeta == null ) {
        return;
      }

      if ( referencedMeta instanceof TransMeta ) {
        TransMeta mappingMeta = (TransMeta) referencedMeta;
        mappingMeta.clearChanged();
        spoon.addTransGraph( mappingMeta );
        TransGraph subTransGraph = spoon.getActiveTransGraph();
        attachActiveTrans( subTransGraph, this.currentStep );
      }

      if ( referencedMeta instanceof JobMeta ) {
        JobMeta jobMeta = (JobMeta) referencedMeta;
        jobMeta.clearChanged();
        spoon.addJobGraph( jobMeta );
        JobGraph jobGraph = spoon.getActiveJobGraph();
        attachActiveJob( jobGraph, this.currentStep );
      }

    } catch ( Exception e ) {
      new ErrorDialog( shell, BaseMessages.getString( PKG, "TransGraph.Exception.UnableToLoadMapping.Title" ),
        BaseMessages.getString( PKG, "TransGraph.Exception.UnableToLoadMapping.Message" ), e );
    }
  }

  /**
   * Finds the last active transformation in the running job to the opened transMeta
   *
   * @param transGraph
   * @param stepMeta
   */
  private void attachActiveTrans( TransGraph transGraph, StepMeta stepMeta ) {
    if ( trans != null && transGraph != null ) {
      Trans subTransformation = trans.getActiveSubTransformation( stepMeta.getName() );
      transGraph.setTrans( subTransformation );
      if ( !transGraph.isExecutionResultsPaneVisible() ) {
        transGraph.showExecutionResults();
      }
      transGraph.setControlStates();
    }
  }

  /**
   * Finds the last active transformation in the running job to the opened transMeta
   *
   * @param transGraph
   * @param stepMeta
   */
  private Trans getActiveSubtransformation( TransGraph transGraph, StepMeta stepMeta ) {
    if ( trans != null && transGraph != null ) {
      return trans.getActiveSubTransformation( stepMeta.getName() );
    }
    return null;
  }

  /**
   * Finds the last active job in the running transformation to the opened jobMeta
   *
   * @param jobGraph
   * @param stepMeta
   */
  private void attachActiveJob( JobGraph jobGraph, StepMeta stepMeta ) {
    if ( trans != null && jobGraph != null ) {
      Job subJob = trans.getActiveSubjobs().get( stepMeta.getName() );
      jobGraph.setJob( subJob );
      if ( !jobGraph.isExecutionResultsPaneVisible() ) {
        jobGraph.showExecutionResults();
      }
      jobGraph.setControlStates();
    }
  }

  /**
   * @return the running
   */
  public boolean isRunning() {
    return running;
  }

  /**
   * @param running the running to set
   */
  public void setRunning( boolean running ) {
    this.running = running;
  }

  /**
   * @return the lastTransDebugMeta
   */
  public TransDebugMeta getLastTransDebugMeta() {
    return lastTransDebugMeta;
  }

  /**
   * @return the halting
   */
  public boolean isHalting() {
    return halting;
  }

  /**
   * @param halting the halting to set
   */
  public void setHalting( boolean halting ) {
    this.halting = halting;
  }

  /**
   * @return the stepLogMap
   */
  public Map<StepMeta, String> getStepLogMap() {
    return stepLogMap;
  }

  /**
   * @param stepLogMap the stepLogMap to set
   */
  public void setStepLogMap( Map<StepMeta, String> stepLogMap ) {
    this.stepLogMap = stepLogMap;
  }

  public void dumpLoggingRegistry() {
    LoggingRegistry registry = LoggingRegistry.getInstance();
    Map<String, LoggingObjectInterface> loggingMap = registry.getMap();

    for ( LoggingObjectInterface loggingObject : loggingMap.values() ) {
      System.out.println( loggingObject.getLogChannelId() + " - " + loggingObject.getObjectName() + " - "
        + loggingObject.getObjectType() );
    }

  }

  @Override
  public HasLogChannelInterface getLogChannelProvider() {
    return new HasLogChannelInterface() {
      @Override
      public LogChannelInterface getLogChannel() {
        return getTrans() != null ? getTrans().getLogChannel() : getTransMeta().getLogChannel();
      }
    };
  }

  public synchronized void setTrans( Trans trans ) {
    this.trans = trans;
    if ( trans != null ) {
      pausing = trans.isPaused();
      initialized = trans.isInitializing();
      running = trans.isRunning();
      halted = trans.isStopped();

      if ( running ) {
        trans.addTransListener( new org.pentaho.di.trans.TransAdapter() {

          @Override
          public void transFinished( Trans trans ) {
            checkTransEnded();
            checkErrorVisuals();
          }
        } );
      }
    }
  }

  public void sniffInput() {
    sniff( true, false, false );
  }

  public void sniffOutput() {
    sniff( false, true, false );
  }

  public void sniffError() {
    sniff( false, false, true );
  }

  public void sniff( final boolean input, final boolean output, final boolean error ) {
    StepMeta stepMeta = getCurrentStep();
    if ( stepMeta == null || trans == null ) {
      return;
    }
    final StepInterface runThread = trans.findRunThread( stepMeta.getName() );
    if ( runThread != null ) {

      List<Object[]> rows = new ArrayList<>();

      final PreviewRowsDialog dialog = new PreviewRowsDialog( shell, trans, SWT.NONE, stepMeta.getName(), null, rows );
      dialog.setDynamic( true );

      // Add a row listener that sends the rows over to the dialog...
      //
      final RowListener rowListener = new RowListener() {

        @Override
        public void rowReadEvent( RowMetaInterface rowMeta, Object[] row ) throws KettleStepException {
          if ( input ) {
            try {
              dialog.addDataRow( rowMeta, rowMeta.cloneRow( row ) );
            } catch ( KettleValueException e ) {
              throw new KettleStepException( e );
            }
          }
        }

        @Override
        public void rowWrittenEvent( RowMetaInterface rowMeta, Object[] row ) throws KettleStepException {
          if ( output ) {
            try {
              dialog.addDataRow( rowMeta, rowMeta.cloneRow( row ) );
            } catch ( KettleValueException e ) {
              throw new KettleStepException( e );
            }
          }
        }

        @Override
        public void errorRowWrittenEvent( RowMetaInterface rowMeta, Object[] row ) throws KettleStepException {
          if ( error ) {
            try {
              dialog.addDataRow( rowMeta, rowMeta.cloneRow( row ) );
            } catch ( KettleValueException e ) {
              throw new KettleStepException( e );
            }
          }
        }
      };

      // When the dialog is closed, make sure to remove the listener!
      //
      dialog.addDialogClosedListener( new DialogClosedListener() {
        @Override
        public void dialogClosed() {
          runThread.removeRowListener( rowListener );
        }
      } );

      // Open the dialog in a separate thread to make sure it doesn't block
      //
      getDisplay().asyncExec( new Runnable() {

        @Override
        public void run() {
          dialog.open();
        }
      } );

      runThread.addRowListener( rowListener );
    }
  }

  @Override
  public String getName() {
    return "transgraph";
  }

  /*
   * (non-Javadoc)
   *
   * @see org.pentaho.ui.xul.impl.XulEventHandler#getXulDomContainer()
   */
  @Override
  public XulDomContainer getXulDomContainer() {
    return xulDomContainer;
  }

  @Override
  public void setName( String arg0 ) {

  }

  /*
   * (non-Javadoc)
   *
   * @see org.pentaho.ui.xul.impl.XulEventHandler#setXulDomContainer(org.pentaho.ui.xul.XulDomContainer)
   */
  @Override
  public void setXulDomContainer( XulDomContainer xulDomContainer ) {
    this.xulDomContainer = xulDomContainer;
  }

  @Override
  public boolean canHandleSave() {
    return true;
  }

  @Override
  public int showChangedWarning() throws KettleException {
    return showChangedWarning( transMeta.getName() );
  }

  private class StepVelocity {
    public StepVelocity( double dx, double dy ) {
      this.dx = dx;
      this.dy = dy;
    }

    public double dx, dy;
  }

  private class StepLocation {
    public StepLocation( double x, double y ) {
      this.x = x;
      this.y = y;
    }

    public double x, y;

    public void add( StepLocation loc ) {
      x += loc.x;
      y += loc.y;
    }
  }

  private class Force {
    public Force( double fx, double fy ) {
      this.fx = fx;
      this.fy = fy;
    }

    public double fx, fy;

    public void add( Force force ) {
      fx += force.fx;
      fy += force.fy;
    }
  }

  private static double dampningConstant = 0.5;
  // private static double springConstant = 1.0;
  private static double timeStep = 1.0;
  private static double nodeMass = 1.0;

  /**
   * Perform an automatic layout of a transformation based on "Force-based algorithms". Source:
   * http://en.wikipedia.org/wiki/Force-based_algorithms_(graph_drawing)
   * <p/>
   * set up initial node velocities to (0,0) set up initial node positions randomly // make sure no 2 nodes are in
   * exactly the same position loop total_kinetic_energy := 0 // running sum of total kinetic energy over all particles
   * for each node net-force := (0, 0) // running sum of total force on this particular node
   * <p/>
   * for each other node net-force := net-force + Coulomb_repulsion( this_node, other_node ) next node
   * <p/>
   * for each spring connected to this node net-force := net-force + Hooke_attraction( this_node, spring ) next spring
   * <p/>
   * // without damping, it moves forever this_node.velocity := (this_node.velocity + timestep * net-force) * damping
   * this_node.position := this_node.position + timestep * this_node.velocity total_kinetic_energy :=
   * total_kinetic_energy + this_node.mass * (this_node.velocity)^2 next node until total_kinetic_energy is less than
   * some small number // the simulation has stopped moving
   */
  public void autoLayout() {
    // Initialize...
    //
    Map<StepMeta, StepVelocity> speeds = new HashMap<>();
    Map<StepMeta, StepLocation> locations = new HashMap<>();
    for ( StepMeta stepMeta : transMeta.getSteps() ) {
      speeds.put( stepMeta, new StepVelocity( 0, 0 ) );
      StepLocation location = new StepLocation( stepMeta.getLocation().x, stepMeta.getLocation().y );
      locations.put( stepMeta, location );
    }
    StepLocation center = calculateCenter( locations );

    // Layout loop!
    //
    double totalKineticEngergy = 0;
    do {
      totalKineticEngergy = 0;

      for ( StepMeta stepMeta : transMeta.getSteps() ) {
        Force netForce = new Force( 0, 0 );
        StepVelocity velocity = speeds.get( stepMeta );
        StepLocation location = locations.get( stepMeta );

        for ( StepMeta otherStep : transMeta.getSteps() ) {
          if ( !stepMeta.equals( otherStep ) ) {
            netForce.add( getCoulombRepulsion( stepMeta, otherStep, locations ) );
          }
        }

        for ( int i = 0; i < transMeta.nrTransHops(); i++ ) {
          TransHopMeta hopMeta = transMeta.getTransHop( i );
          if ( hopMeta.getFromStep().equals( stepMeta ) || hopMeta.getToStep().equals( stepMeta ) ) {
            netForce.add( getHookeAttraction( hopMeta, locations ) );
          }
        }

        adjustVelocity( velocity, netForce );
        adjustLocation( location, velocity );
        totalKineticEngergy += nodeMass * ( velocity.dx * velocity.dx + velocity.dy * velocity.dy );
      }

      StepLocation newCenter = calculateCenter( locations );
      StepLocation diff = new StepLocation( center.x - newCenter.x, center.y - newCenter.y );
      for ( StepMeta stepMeta : transMeta.getSteps() ) {
        StepLocation location = locations.get( stepMeta );
        location.x += diff.x;
        location.y += diff.y;
        stepMeta.setLocation( (int) Math.round( location.x ), (int) Math.round( location.y ) );
      }

      // redraw...
      //
      redraw();

    } while ( totalKineticEngergy < 0.01 );
  }

  private StepLocation calculateCenter( Map<StepMeta, StepLocation> locations ) {
    StepLocation center = new StepLocation( 0, 0 );
    for ( StepLocation location : locations.values() ) {
      center.add( location );
    }
    center.x /= locations.size();
    center.y /= locations.size();
    return center;
  }

  /**
   * http://en.wikipedia.org/wiki/Coulomb's_law
   *
   * @param step1
   * @param step2
   * @param locations
   * @return
   */
  private Force getCoulombRepulsion( StepMeta step1, StepMeta step2, Map<StepMeta, StepLocation> locations ) {
    double q1 = 4.0;
    double q2 = 4.0;
    double Ke = -3.0;
    StepLocation loc1 = locations.get( step1 );
    StepLocation loc2 = locations.get( step2 );

    double fx = Ke * q1 * q2 / Math.abs( loc1.x - loc2.x );
    double fy = Ke * q1 * q2 / Math.abs( loc1.y - loc2.y );

    return new Force( fx, fy );
  }

  /**
   * The longer the hop, the higher the force
   *
   * @param hopMeta
   * @param locations
   * @return
   */
  private Force getHookeAttraction( TransHopMeta hopMeta, Map<StepMeta, StepLocation> locations ) {
    StepLocation loc1 = locations.get( hopMeta.getFromStep() );
    StepLocation loc2 = locations.get( hopMeta.getToStep() );
    double springConstant = 0.01;

    double fx = springConstant * Math.abs( loc1.x - loc2.x );
    double fy = springConstant * Math.abs( loc1.y - loc2.y );

    return new Force( fx * fx, fy * fy );
  }

  private void adjustVelocity( StepVelocity velocity, Force netForce ) {
    velocity.dx = ( velocity.dx + timeStep * netForce.fx ) * dampningConstant;
    velocity.dy = ( velocity.dy + timeStep * netForce.fy ) * dampningConstant;
  }

  private void adjustLocation( StepLocation location, StepVelocity velocity ) {
    location.x = location.x + nodeMass * velocity.dx * velocity.dx;
    location.y = location.y + nodeMass * velocity.dy * velocity.dy;
  }

  public void handleTransMetaChanges( TransMeta transMeta ) throws KettleException {
    if ( transMeta.hasChanged() ) {
      if ( spoon.props.getAutoSave() ) {
        spoon.saveToFile( transMeta );
      } else {
        MessageDialogWithToggle md =
          new MessageDialogWithToggle( shell, BaseMessages.getString( PKG, "TransLog.Dialog.FileHasChanged.Title" ),
            null, BaseMessages.getString( PKG, "TransLog.Dialog.FileHasChanged1.Message" ) + Const.CR
            + BaseMessages.getString( PKG, "TransLog.Dialog.FileHasChanged2.Message" ) + Const.CR,
            MessageDialog.QUESTION, new String[] { BaseMessages.getString( PKG, "System.Button.Yes" ),
            BaseMessages.getString( PKG, "System.Button.No" ) }, 0, BaseMessages.getString( PKG,
            "TransLog.Dialog.Option.AutoSaveTransformation" ), spoon.props.getAutoSave() );
        MessageDialogWithToggle.setDefaultImage( GUIResource.getInstance().getImageSpoon() );
        int answer = md.open();
        if ( ( answer & 0xFF ) == 0 ) {
          spoon.saveToFile( transMeta );
        }
        spoon.props.setAutoSave( md.getToggleState() );
      }
    }
  }

  private StepMeta lastChained = null;

  public void addStepToChain( PluginInterface stepPlugin, boolean shift ) {
    TransMeta transMeta = spoon.getActiveTransformation();
    if ( transMeta == null ) {
      return;
    }

    // Is the lastChained entry still valid?
    //
    if ( lastChained != null && transMeta.findStep( lastChained.getName() ) == null ) {
      lastChained = null;
    }

    // If there is exactly one selected step, pick that one as last chained.
    //
    List<StepMeta> sel = transMeta.getSelectedSteps();
    if ( sel.size() == 1 ) {
      lastChained = sel.get( 0 );
    }

    // Where do we add this?

    Point p = null;
    if ( lastChained == null ) {
      p = transMeta.getMaximum();
      p.x -= 100;
    } else {
      p = new Point( lastChained.getLocation().x, lastChained.getLocation().y );
    }

    p.x += 200;

    // Which is the new step?

    StepMeta newStep =
      spoon.newStep( transMeta, stepPlugin.getIds()[ 0 ], stepPlugin.getName(), stepPlugin.getName(), false, true );
    if ( newStep == null ) {
      return;
    }
    newStep.setLocation( p.x, p.y );
    newStep.setDraw( true );

    if ( lastChained != null ) {
      TransHopMeta hop = new TransHopMeta( lastChained, newStep );
      spoon.newHop( transMeta, hop );
    }

    lastChained = newStep;
    spoon.refreshGraph();
    spoon.refreshTree();

    if ( shift ) {
      editStep( newStep );
    }

    transMeta.unselectAll();
    newStep.setSelected( true );

  }

  public Spoon getSpoon() {
    return spoon;
  }

  public void setSpoon( Spoon spoon ) {
    this.spoon = spoon;
  }

  public TransMeta getTransMeta() {
    return transMeta;
  }

  public Trans getTrans() {
    return trans;
  }

  private Trans createLegacyTrans() {
    try {
      return new Trans( transMeta, spoon.rep, transMeta.getName(),
        transMeta.getRepositoryDirectory().getPath(),
        transMeta.getFilename() );
    } catch ( KettleException e ) {
      throw new RuntimeException( e );
    }
  }

  private void setHopEnabled( TransHopMeta hop, boolean enabled ) {
    hop.setEnabled( enabled );
    transMeta.clearCaches();
  }

  private void modalMessageDialog( String title, String message, int swtFlags ) {
    MessageBox messageBox = new MessageBox( shell, swtFlags );
    messageBox.setMessage( message );
    messageBox.setText( title );
    messageBox.open();
  }

  private String getString( String key ) {
    return BaseMessages.getString( PKG, key );
  }

  private String getString( String key, String... params ) {
    return BaseMessages.getString( PKG, key, params );
  }
}
