// CHECKSTYLE:FileLength:OFF
/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2013 by Pentaho : 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.job;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.Callable;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
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.Color;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
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.Const;
import org.pentaho.di.core.EngineMetaInterface;
import org.pentaho.di.core.NotePadMeta;
import org.pentaho.di.core.Props;
import org.pentaho.di.core.Result;
import org.pentaho.di.core.ResultFile;
import org.pentaho.di.core.RowMetaAndData;
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.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.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.HasLogChannelInterface;
import org.pentaho.di.core.logging.KettleLogStore;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.logging.LogParentProvidedInterface;
import org.pentaho.di.core.logging.LoggingObjectType;
import org.pentaho.di.core.logging.SimpleLoggingObject;
import org.pentaho.di.core.vfs.KettleVFS;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.job.Job;
import org.pentaho.di.job.JobAdapter;
import org.pentaho.di.job.JobEntryListener;
import org.pentaho.di.job.JobEntryResult;
import org.pentaho.di.job.JobExecutionConfiguration;
import org.pentaho.di.job.JobHopMeta;
import org.pentaho.di.job.JobMeta;
import org.pentaho.di.job.JobPainter;
import org.pentaho.di.job.entries.abort.JobEntryAbort;
import org.pentaho.di.job.entries.job.JobEntryJob;
import org.pentaho.di.job.entries.trans.JobEntryTrans;
import org.pentaho.di.job.entry.JobEntryCopy;
import org.pentaho.di.job.entry.JobEntryInterface;
import org.pentaho.di.repository.KettleRepositoryLostException;
import org.pentaho.di.repository.Repository;
import org.pentaho.di.repository.RepositoryDirectoryInterface;
import org.pentaho.di.repository.RepositoryObjectType;
import org.pentaho.di.repository.RepositoryOperation;
import org.pentaho.di.shared.SharedObjects;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.TransPainter;
import org.pentaho.di.ui.core.ConstUI;
import org.pentaho.di.ui.core.PropsUI;
import org.pentaho.di.ui.core.dialog.EnterTextDialog;
import org.pentaho.di.ui.core.dialog.ErrorDialog;
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.job.dialog.JobDialog;
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.SwtScrollBar;
import org.pentaho.di.ui.spoon.TabItemInterface;
import org.pentaho.di.ui.spoon.TabMapEntry;
import org.pentaho.di.ui.spoon.TabMapEntry.ObjectType;
import org.pentaho.di.ui.spoon.XulSpoonResourceBundle;
import org.pentaho.di.ui.spoon.XulSpoonSettingsManager;
import org.pentaho.di.ui.spoon.dialog.NotePadDialog;
import org.pentaho.di.ui.spoon.trans.DelayListener;
import org.pentaho.di.ui.spoon.trans.DelayTimer;
import org.pentaho.di.ui.spoon.trans.TransGraph;
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;

/**
 * Handles the display of Jobs in Spoon, in a graphical form.
 *
 * @author Matt Created on 17-may-2003
 *
 */
public class JobGraph extends AbstractGraph implements XulEventHandler, Redrawable, TabItemInterface,
  LogParentProvidedInterface, MouseListener, MouseMoveListener, MouseTrackListener, MouseWheelListener,
  KeyListener {

  private static Class<?> PKG = JobGraph.class; // for i18n purposes, needed by Translator2!!

  private static final String XUL_FILE_JOB_GRAPH = "ui/job-graph.xul";

  public static final String START_TEXT = BaseMessages.getString( PKG, "JobLog.Button.Start" );
  public static final String STOP_TEXT = BaseMessages.getString( PKG, "JobLog.Button.Stop" );

  private static final String STRING_PARALLEL_WARNING_PARAMETER = "ParallelJobEntriesWarning";

  private static final int HOP_SEL_MARGIN = 9;

  protected Shell shell;

  protected LogChannelInterface log;

  protected JobMeta jobMeta;

  public Job job;

  protected PropsUI props;

  protected int iconsize;

  protected int linewidth;

  protected Point lastclick;

  protected List<JobEntryCopy> selectedEntries;

  protected JobEntryCopy selectedEntry;

  protected Point[] previousLocations;

  private List<NotePadMeta> selectedNotes;
  protected NotePadMeta selectedNote;

  protected Point previous_note_location;

  protected Point lastMove;

  protected JobHopMeta hop_candidate;

  protected Point drop_candidate;

  protected Spoon spoon;

  // public boolean shift, control;
  protected boolean split_hop;

  protected int lastButton;

  protected JobHopMeta last_hop_split;

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

  protected static final double theta = Math.toRadians( 10 ); // arrowhead sharpness

  protected static final int size = 30; // arrowhead length

  protected int shadowsize;

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

  protected int currentMouseX = 0;

  protected int currentMouseY = 0;

  protected JobEntryCopy jobEntry;

  protected NotePadMeta ni = null;

  protected JobHopMeta currentHop;

  // private Text filenameLabel;
  private SashForm sashForm;

  public Composite extraViewComposite;

  public CTabFolder extraViewTabFolder;

  private XulToolbar toolbar;

  public JobLogDelegate jobLogDelegate;

  public JobHistoryDelegate jobHistoryDelegate;

  public JobGridDelegate jobGridDelegate;

  public JobMetricsDelegate jobMetricsDelegate;

  private Composite mainComposite;

  private List<RefreshListener> refreshListeners;

  private Label closeButton;

  private Label minMaxButton;

  private CheckBoxToolTip helpTip;

  private List<AreaOwner> areaOwners;

  private List<JobEntryCopy> mouseOverEntries;

  /** A map that keeps track of which log line was written by which job entry */
  private Map<JobEntryCopy, String> entryLogMap;

  private Map<JobEntryCopy, DelayTimer> delayTimers;

  private JobEntryCopy startHopEntry;
  private Point endHopLocation;

  private JobEntryCopy endHopEntry;
  private JobEntryCopy noInputEntry;
  private DefaultToolTip toolTip;
  private Point[] previous_step_locations;
  private Point[] previous_note_locations;
  private JobEntryCopy currentEntry;

  public JobGraph( Composite par, final Spoon spoon, final JobMeta jobMeta ) {
    super( par, SWT.NONE );
    shell = par.getShell();
    this.log = spoon.getLog();
    this.spoon = spoon;
    this.jobMeta = jobMeta;
    spoon.selectionFilter.setText( "" );

    this.props = PropsUI.getInstance();
    this.areaOwners = new ArrayList<AreaOwner>();
    this.mouseOverEntries = new ArrayList<JobEntryCopy>();
    this.delayTimers = new HashMap<JobEntryCopy, DelayTimer>();

    jobLogDelegate = new JobLogDelegate( spoon, this );
    jobHistoryDelegate = new JobHistoryDelegate( spoon, this );
    jobGridDelegate = new JobGridDelegate( spoon, this );
    jobMetricsDelegate = new JobMetricsDelegate( spoon, this );

    refreshListeners = new ArrayList<RefreshListener>();

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

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

      setXulDomContainer( container );
    } catch ( XulException e1 ) {
      log.logError( toString(), Const.getStackTracker( 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();

    // 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 {
      Document doc = xulDomContainer.getDocumentRoot();
      menuMap.put( "job-graph-hop", (XulMenupopup) doc.getElementById( "job-graph-hop" ) );
      menuMap.put( "job-graph-note", (XulMenupopup) doc.getElementById( "job-graph-note" ) );
      menuMap.put( "job-graph-background", (XulMenupopup) doc.getElementById( "job-graph-background" ) );
      menuMap.put( "job-graph-entry", (XulMenupopup) doc.getElementById( "job-graph-entry" ) );
    } catch ( Throwable t ) {
      log.logError( Const.getStackTracker( t ) );
      new ErrorDialog(
        shell, BaseMessages.getString( PKG, "JobGraph.Exception.ErrorReadingXULFile.Title" ), BaseMessages
          .getString( PKG, "JobGraph.Exception.ErrorReadingXULFile.Message", Spoon.XUL_FILE_MENUS ),
        new Exception( t ) );
    }

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

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

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

    newProps();

    selectionRegion = null;
    hop_candidate = null;
    last_hop_split = null;

    selectedEntries = null;
    selectedNote = null;

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

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

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

    setVisible( true );

    canvas.addPaintListener( new PaintListener() {
      public void paintControl( PaintEvent e ) {
        JobGraph.this.paintControl( e );
      }
    } );

    selectedEntries = null;
    lastclick = null;

    canvas.addMouseListener( this );
    canvas.addMouseMoveListener( this );
    canvas.addMouseTrackListener( this );
    canvas.addMouseWheelListener( 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() {
      public void dragEnter( DropTargetEvent event ) {
        drop_candidate = PropsUI.calculateGridPosition( getRealPosition( canvas, event.x, event.y ) );
        redraw();
      }

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

      public void dragOperationChanged( DropTargetEvent event ) {
      }

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

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

        Point p = getRealPosition( canvas, event.x, event.y );

        try {
          DragAndDropContainer container = (DragAndDropContainer) event.data;
          String entry = container.getData();

          switch ( container.getType() ) {
            case DragAndDropContainer.TYPE_BASE_JOB_ENTRY: // Create a new Job Entry on the canvas
              JobEntryCopy jge = spoon.newJobEntry( jobMeta, entry, false );
              if ( jge != null ) {
                PropsUI.setLocation( jge, p.x, p.y );
                jge.setDrawn();
                redraw();

                // See if we want to draw a tool tip explaining how to create new hops...
                //
                if ( jobMeta.nrJobEntries() > 1
                  && jobMeta.nrJobEntries() < 5 && spoon.props.isShowingHelpToolTips() ) {
                  showHelpTip(
                    p.x, p.y, BaseMessages.getString( PKG, "JobGraph.HelpToolTip.CreatingHops.Title" ),
                    BaseMessages.getString( PKG, "JobGraph.HelpToolTip.CreatingHops.Message" ) );
                }
              }
              break;

            case DragAndDropContainer.TYPE_JOB_ENTRY: // Drag existing one onto the canvas
              jge = jobMeta.findJobEntry( entry, 0, true );
              if ( jge != null ) {
                // Create duplicate of existing entry

                // There can be only 1 start!
                if ( jge.isStart() && jge.isDrawn() ) {
                  showOnlyStartOnceMessage( shell );
                  return;
                }

                boolean jge_changed = false;

                // For undo :
                JobEntryCopy before = (JobEntryCopy) jge.clone_deep();

                JobEntryCopy newjge = jge;
                if ( jge.isDrawn() ) {
                  newjge = (JobEntryCopy) jge.clone();
                  if ( newjge != null ) {
                    // newjge.setEntry(jge.getEntry());
                    if ( log.isDebug() ) {
                      log.logDebug( "entry aft = " + ( (Object) jge.getEntry() ).toString() );
                    }

                    newjge.setNr( jobMeta.findUnusedNr( newjge.getName() ) );

                    jobMeta.addJobEntry( newjge );
                    spoon.addUndoNew( jobMeta, new JobEntryCopy[] { newjge }, new int[] { jobMeta
                      .indexOfJobEntry( newjge ) } );
                  } else {
                    if ( log.isDebug() ) {
                      log.logDebug( "jge is not cloned!" );
                    }
                  }
                } else {
                  if ( log.isDebug() ) {
                    log.logDebug( jge.toString() + " is not drawn" );
                  }
                  jge_changed = true;
                }
                PropsUI.setLocation( newjge, p.x, p.y );
                newjge.setDrawn();
                if ( jge_changed ) {
                  spoon.addUndoChange(
                    jobMeta, new JobEntryCopy[] { before }, new JobEntryCopy[] { newjge }, new int[] { jobMeta
                      .indexOfJobEntry( newjge ) } );
                }
                redraw();
                spoon.refreshTree();
                log.logBasic( "DropTargetEvent", "DROP "
                  + newjge.toString() + "!, type=" + newjge.getEntry().getPluginId() );
              } else {
                log.logError( "Unknown job entry dropped onto the canvas." );
              }
              break;

            default:
              break;
          }
        } catch ( Exception e ) {
          new ErrorDialog(
            shell, BaseMessages.getString( PKG, "JobGraph.Dialog.ErrorDroppingObject.Message" ), BaseMessages
              .getString( PKG, "JobGraph.Dialog.ErrorDroppingObject.Title" ), e );
        }
      }

      public void dropAccept( DropTargetEvent event ) {
        drop_candidate = null;
      }
    } );

    canvas.addKeyListener( this );

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

    setControlStates();

    // Add a timer to set correct the state of the run/stop buttons every 2 seconds...
    //
    final Timer timer = new Timer( "JobGraph.setControlStates Timer: " + getMeta().getName() );
    TimerTask timerTask = new TimerTask() {
      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() {
      public void widgetDisposed( DisposeEvent arg0 ) {
        timer.cancel();
      }
    } );

  }

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

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

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

    // Hide the tooltip!
    hideToolTips();

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

    JobEntryCopy jobentry = jobMeta.getJobEntryCopy( real.x, real.y, iconsize );
    if ( jobentry != null ) {
      if ( e.button == 1 ) {
        editEntry( jobentry );
      } else {
        // open tab in Spoon
        launchStuff( jobentry );
      }
    } else {
      // Check if point lies on one of the many hop-lines...
      JobHopMeta online = findJobHop( real.x, real.y );
      if ( online == null ) {
        NotePadMeta ni = jobMeta.getNote( real.x, real.y );
        if ( ni != null ) {
          editNote( ni );
        } else {
          // Clicked on the background...
          //
          editJobProperties();
        }
      }

    }
  }

  public void mouseDown( MouseEvent e ) {

    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.JobGraphMouseDown.id,
        new JobGraphExtension( this, e, real ) );
    } catch ( Exception ex ) {
      LogChannel.GENERAL.logError( "Error calling JobGraphMouseDown 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 ) {
        switch ( areaOwner.getAreaType() ) {
          case JOB_ENTRY_MINI_ICON_OUTPUT:
            // Click on the output icon means: start of drag
            // Action: We show the input icons on the other steps...
            //
            selectedEntry = null;
            startHopEntry = (JobEntryCopy) areaOwner.getOwner();
            // stopEntryMouseOverDelayTimer(startHopEntry);
            break;

          case JOB_ENTRY_MINI_ICON_INPUT:
            // Click on the input icon means: start to a new hop
            // In this case, we set the end hop step...
            //
            selectedEntry = null;
            startHopEntry = null;
            endHopEntry = (JobEntryCopy) areaOwner.getOwner();
            // stopEntryMouseOverDelayTimer(endHopEntry);
            break;

          case JOB_ENTRY_MINI_ICON_EDIT:
            clearSettings();
            currentEntry = (JobEntryCopy) areaOwner.getOwner();
            stopEntryMouseOverDelayTimer( currentEntry );
            editEntry( currentEntry );
            break;

          case JOB_ENTRY_MINI_ICON_CONTEXT:
            clearSettings();
            JobEntryCopy jobEntryCopy = (JobEntryCopy) areaOwner.getOwner();
            setMenu( jobEntryCopy.getLocation().x, jobEntryCopy.getLocation().y );
            break;

          case JOB_ENTRY_ICON:
            jobEntryCopy = (JobEntryCopy) areaOwner.getOwner();
            currentEntry = jobEntryCopy;

            if ( hop_candidate != null ) {
              addCandidateAsHop();

            } else if ( e.button == 2 || ( e.button == 1 && shift ) ) {
              // SHIFT CLICK is start of drag to create a new hop
              //
              startHopEntry = jobEntryCopy;

            } else {
              selectedEntries = jobMeta.getSelectedEntries();
              selectedEntry = jobEntryCopy;
              //
              // 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 = jobMeta.getSelectedLocations();

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

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

            previous_note_locations = jobMeta.getSelectedNoteLocations();

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

            redraw();
            break;

          // If you click on an evaluating icon, change the evaluation...
          //
          case JOB_HOP_ICON:
            JobHopMeta hop = (JobHopMeta) areaOwner.getOwner();
            if ( hop.getFromEntry().evaluates() ) {
              if ( hop.isUnconditional() ) {
                hop.setUnconditional( false );
                hop.setEvaluation( true );
              } else {
                if ( hop.getEvaluation() ) {
                  hop.setEvaluation( false );
                } else {
                  hop.setUnconditional( true );
                }
              }
              spoon.setShellText();
              redraw();
            }
            break;
          default:
            break;
        }
      } else {
        // A hop? --> enable/disable
        //
        JobHopMeta hop = findJobHop( real.x, real.y );
        if ( hop != null ) {
          JobHopMeta before = (JobHopMeta) hop.clone();
          hop.setEnabled( !hop.isEnabled() );
          JobHopMeta after = (JobHopMeta) hop.clone();
          spoon.addUndoChange(
            jobMeta, new JobHopMeta[] { before }, new JobHopMeta[] { after }, new int[] { jobMeta
              .indexOfJobHop( hop ) } );
          spoon.setShellText();
          redraw();
        } else {
          // No area-owner means: background:
          //
          startHopEntry = null;
          if ( !control ) {
            selectionRegion = new org.pentaho.di.core.gui.Rectangle( real.x, real.y, 0, 0 );
          }
          redraw();
        }
      }
    }
  }

  public void mouseUp( MouseEvent e ) {
    boolean control = ( e.stateMask & SWT.MOD1 ) != 0;

    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 );

    // Quick new hop option? (drag from one step to another)
    //
    if ( hop_candidate != null ) {
      addCandidateAsHop();
      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;

        jobMeta.unselectAll();
        selectInRect( jobMeta, selectionRegion );
        selectionRegion = null;
        stopEntryMouseOverDelayTimers();
        redraw();
      } else {
        // Clicked on an icon?
        //
        if ( selectedEntry != null && startHopEntry == 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 ) {
                selectedEntry.flipSelected();
              } else {
                // Otherwise, select only the icon clicked on!
                jobMeta.unselectAll();
                selectedEntry.setSelected( true );
              }
            } else {
              // Find out which Steps & Notes are selected
              selectedEntries = jobMeta.getSelectedEntries();
              selectedNotes = jobMeta.getSelectedNotes();

              // We moved around some items: store undo info...
              //
              boolean also = false;
              if ( selectedNotes != null && selectedNotes.size() > 0 && previous_note_locations != null ) {
                int[] indexes = jobMeta.getNoteIndexes( selectedNotes );

                addUndoPosition(
                  selectedNotes.toArray( new NotePadMeta[selectedNotes.size()] ), indexes,
                  previous_note_locations, jobMeta.getSelectedNoteLocations(), also );
                also = selectedEntries != null && selectedEntries.size() > 0;
              }
              if ( selectedEntries != null && selectedEntries.size() > 0 && previous_step_locations != null ) {
                int[] indexes = jobMeta.getEntryIndexes( selectedEntries );
                addUndoPosition(
                  selectedEntries.toArray( new JobEntryCopy[selectedEntries.size()] ), indexes,
                  previous_step_locations, jobMeta.getSelectedLocations(), also );
              }
            }
          }

          // OK, we moved the step, did we move it across a hop?
          // If so, ask to split the hop!
          if ( split_hop ) {
            JobHopMeta hi = findHop( icon.x + iconsize / 2, icon.y + iconsize / 2, selectedEntry );
            if ( hi != null ) {
              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.getFromEntry()
                // B ==> hi.getToEntry();
                // C ==> selected_step
                //
                if ( jobMeta.findJobHop( selectedEntry, hi.getFromEntry() ) == null
                  && jobMeta.findJobHop( hi.getToEntry(), selectedEntry ) == null ) {

                  if ( jobMeta.findJobHop( hi.getFromEntry(), selectedEntry, true ) == null ) {
                    JobHopMeta newhop1 = new JobHopMeta( hi.getFromEntry(), selectedEntry );
                    if ( hi.getFromEntry().getEntry().isUnconditional() ) {
                      newhop1.setUnconditional();
                    }
                    jobMeta.addJobHop( newhop1 );
                    spoon.addUndoNew( jobMeta, new JobHopMeta[] { newhop1, }, new int[] { jobMeta
                      .indexOfJobHop( newhop1 ), }, true );
                  }
                  if ( jobMeta.findJobHop( selectedEntry, hi.getToEntry(), true ) == null ) {
                    JobHopMeta newhop2 = new JobHopMeta( selectedEntry, hi.getToEntry() );
                    if ( selectedEntry.getEntry().isUnconditional() ) {
                      newhop2.setUnconditional();
                    }
                    jobMeta.addJobHop( newhop2 );
                    spoon.addUndoNew( jobMeta, new JobHopMeta[] { newhop2, }, new int[] { jobMeta
                      .indexOfJobHop( newhop2 ), }, true );
                  }

                  int idx = jobMeta.indexOfJobHop( hi );
                  spoon.addUndoDelete( jobMeta, new JobHopMeta[] { hi }, new int[] { idx }, true );
                  jobMeta.removeJobHop( idx );
                  spoon.refreshTree();

                }
                // else: Silently discard this hop-split attempt.
              }
            }
            split_hop = false;
          }

          selectedEntries = null;
          selectedNotes = null;
          selectedEntry = null;
          selectedNote = null;
          startHopEntry = 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!
                  jobMeta.unselectAll();
                  selectedNote.setSelected( true );
                }
              } else {
                // Find out which Steps & Notes are selected
                selectedEntries = jobMeta.getSelectedEntries();
                selectedNotes = jobMeta.getSelectedNotes();

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

            selectedNotes = null;
            selectedEntries = null;
            selectedEntry = null;
            selectedNote = null;
            startHopEntry = null;
            endHopLocation = null;
          } else {
            AreaOwner areaOwner = getVisibleAreaOwner( real.x, real.y );
            if ( areaOwner == null && selectionRegion == null ) {
              // Hit absolutely nothing: clear the settings
              //
              clearSettings();
            }
          }
        }
      }
    }

    lastButton = 0;
  }

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

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

    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 ) {
      JobEntryCopy jobEntryCopy = null;
      switch ( areaOwner.getAreaType() ) {
        case JOB_ENTRY_ICON:
          jobEntryCopy = (JobEntryCopy) areaOwner.getOwner();
          resetDelayTimer( jobEntryCopy );
          break;
        case MINI_ICONS_BALLOON: // Give the timer a bit more time
          jobEntryCopy = (JobEntryCopy) areaOwner.getOwner();
          resetDelayTimer( jobEntryCopy );
          break;
        default:
          break;
      }
    }

    //
    // 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 ( selectedEntry != null && !selectedEntry.isSelected() ) {
      jobMeta.unselectAll();
      selectedEntry.setSelected( true );
      selectedEntries = new ArrayList<JobEntryCopy>();
      selectedEntries.add( selectedEntry );
      previous_step_locations = new Point[] { selectedEntry.getLocation() };
      redraw();
    } else if ( selectedNote != null && !selectedNote.isSelected() ) {
      jobMeta.unselectAll();
      selectedNote.setSelected( true );
      selectedNotes = new ArrayList<NotePadMeta>();
      selectedNotes.add( selectedNote );
      previous_note_locations = new Point[] { selectedNote.getLocation() };
      redraw();
    } else if ( selectionRegion != null && startHopEntry == null ) {
      // Did we select a region...?
      //
      selectionRegion.width = real.x - selectionRegion.x;
      selectionRegion.height = real.y - selectionRegion.y;
      redraw();
    } else if ( selectedEntry != null && lastButton == 1 && !shift && startHopEntry == null ) {
      // Move around steps & notes
      //
      //
      // 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 - selectedEntry.getLocation().x;
      int dy = icon.y - selectedEntry.getLocation().y;

      // See if we have a hop-split candidate
      //
      JobHopMeta hi = findHop( icon.x + iconsize / 2, icon.y + iconsize / 2, selectedEntry );
      if ( hi != null ) {
        // OK, we want to split the hop in 2
        //
        if ( !hi.getFromEntry().equals( selectedEntry ) && !hi.getToEntry().equals( selectedEntry ) ) {
          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 = jobMeta.getSelectedNotes();
      selectedEntries = jobMeta.getSelectedEntries();

      // Adjust location of selected steps...
      if ( selectedEntries != null ) {
        for ( int i = 0; i < selectedEntries.size(); i++ ) {
          JobEntryCopy jobEntryCopy = selectedEntries.get( i );
          PropsUI.setLocation( jobEntryCopy, jobEntryCopy.getLocation().x + dx, jobEntryCopy.getLocation().y + dy );
          stopEntryMouseOverDelayTimer( jobEntryCopy );
        }
      }
      // 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 ( ( startHopEntry != null && endHopEntry == null )
      || ( endHopEntry != null && startHopEntry == null ) ) {
      // Are we creating a new hop with the middle button or pressing SHIFT?
      //

      JobEntryCopy jobEntryCopy = jobMeta.getJobEntryCopy( real.x, real.y, iconsize );
      endHopLocation = new Point( real.x, real.y );
      if ( jobEntryCopy != null
        && ( ( startHopEntry != null && !startHopEntry.equals( jobEntryCopy ) ) || ( endHopEntry != null && !endHopEntry
          .equals( jobEntryCopy ) ) ) ) {
        if ( hop_candidate == null ) {
          // See if the step accepts input. If not, we can't create a new hop...
          //
          if ( startHopEntry != null ) {
            if ( !jobEntryCopy.isStart() ) {
              hop_candidate = new JobHopMeta( startHopEntry, jobEntryCopy );
              endHopLocation = null;
            } else {
              noInputEntry = jobEntryCopy;
              toolTip.setImage( null );
              toolTip.setText( "The start entry can only be used at the start of a Job" );
              toolTip.show( new org.eclipse.swt.graphics.Point( real.x, real.y ) );
            }
          } else if ( endHopEntry != null ) {
            hop_candidate = new JobHopMeta( jobEntryCopy, endHopEntry );
            endHopLocation = null;
          }
        }
      } else {
        if ( hop_candidate != null ) {
          hop_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 = jobMeta.getSelectedNotes();
        selectedEntries = jobMeta.getSelectedEntries();

        // Adjust location of selected steps...
        if ( selectedEntries != null ) {
          for ( int i = 0; i < selectedEntries.size(); i++ ) {
            JobEntryCopy jobEntryCopy = selectedEntries.get( i );
            PropsUI.setLocation( jobEntryCopy, jobEntryCopy.getLocation().x + dx, jobEntryCopy.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();
      }
    }
  }

  public void mouseHover( MouseEvent e ) {

    boolean tip = true;

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

    AreaOwner areaOwner = getVisibleAreaOwner( real.x, real.y );
    if ( areaOwner != null ) {
      switch ( areaOwner.getAreaType() ) {
        case JOB_ENTRY_ICON:
          JobEntryCopy jobEntryCopy = (JobEntryCopy) areaOwner.getOwner();
          if ( !jobEntryCopy.isMissing() && !mouseOverEntries.contains( jobEntryCopy ) ) {
            addEntryMouseOverDelayTimer( jobEntryCopy );
            redraw();
            tip = false;
          }
          break;
        default:
          break;
      }
    }

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

  public void mouseEnter( MouseEvent event ) {
  }

  public void mouseExit( MouseEvent event ) {
  }

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

  private void addCandidateAsHop() {
    if ( hop_candidate != null ) {

      if ( !hop_candidate.getFromEntry().evaluates() && hop_candidate.getFromEntry().isUnconditional() ) {
        hop_candidate.setUnconditional();
      } else {
        hop_candidate.setConditional();
        int nr = jobMeta.findNrNextJobEntries( hop_candidate.getFromEntry() );

        // If there is one green link: make this one red! (or
        // vice-versa)
        if ( nr == 1 ) {
          JobEntryCopy jge = jobMeta.findNextJobEntry( hop_candidate.getFromEntry(), 0 );
          JobHopMeta other = jobMeta.findJobHop( hop_candidate.getFromEntry(), jge );
          if ( other != null ) {
            hop_candidate.setEvaluation( !other.getEvaluation() );
          }
        }
      }

      if ( checkIfHopAlreadyExists( jobMeta, hop_candidate ) ) {
        boolean cancel = false;
        jobMeta.addJobHop( hop_candidate );
        if ( jobMeta.hasLoop( hop_candidate.getFromEntry() ) || jobMeta.hasLoop( hop_candidate.getToEntry() ) ) {
          MessageBox mb = new MessageBox( spoon.getShell(), SWT.OK | SWT.CANCEL | SWT.ICON_WARNING );
          mb.setMessage( BaseMessages.getString( PKG, "JobGraph.Dialog.HopCausesLoop.Message" ) );
          mb.setText( BaseMessages.getString( PKG, "JobGraph.Dialog.HopCausesLoop.Title" ) );
          int choice = mb.open();
          if ( choice == SWT.CANCEL ) {
            jobMeta.removeJobHop( hop_candidate );
            cancel = true;
          }
        }
        if ( !cancel ) {
          spoon.addUndoNew( jobMeta, new JobHopMeta[] { hop_candidate }, new int[] { jobMeta
            .indexOfJobHop( hop_candidate ) } );
        }
        spoon.refreshTree();
        clearSettings();
        redraw();
      }
    }
  }

  public boolean checkIfHopAlreadyExists( JobMeta jobMeta, JobHopMeta newHop ) {
    boolean ok = true;
    if ( jobMeta.findJobHop( newHop.getFromEntry(), newHop.getToEntry(), true ) != null ) {
      MessageBox mb = new MessageBox( shell, SWT.OK | SWT.ICON_ERROR );
      mb.setMessage( BaseMessages.getString( PKG, "JobGraph.Dialog.HopExists.Message" ) ); // "This hop already exists!"
      mb.setText( BaseMessages.getString( PKG, "JobGraph.Dialog.HopExists.Title" ) ); // Error!
      mb.open();
      ok = false;
    }

    return ok;
  }

  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;
  }

  private synchronized void addEntryMouseOverDelayTimer( final JobEntryCopy jobEntryCopy ) {

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

    mouseOverEntries.add( jobEntryCopy );

    DelayTimer delayTimer = new DelayTimer( 500, new DelayListener() {
      public void expired() {
        mouseOverEntries.remove( jobEntryCopy );
        delayTimers.remove( jobEntryCopy );
        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.JOB_ENTRY_ICON || areaType.belongsToJobContextMenu() ) {
              JobEntryCopy selectedJobEntryCopy = (JobEntryCopy) areaOwner.getOwner();
              return selectedJobEntryCopy == jobEntryCopy;
            }
          }
        }
        return false;
      }
    } );

    new Thread( delayTimer ).start();

    delayTimers.put( jobEntryCopy, delayTimer );
  }

  private void stopEntryMouseOverDelayTimer( final JobEntryCopy jobEntryCopy ) {
    DelayTimer delayTimer = delayTimers.get( jobEntryCopy );
    if ( delayTimer != null ) {
      delayTimer.stop();
    }
  }

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

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

  protected void asyncRedraw() {
    spoon.getDisplay().asyncExec( new Runnable() {
      public void run() {
        if ( !isDisposed() ) {
          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 ) {
                runJob();
              }
            } );
            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 ) {
                runOptionsJob();
              }
            } );

            menu.setLocation( shell.getDisplay().map( mainComposite.getParent(), null, mainComposite.getLocation() ) );
            menu.setVisible( true );
          } else {
            runJob();
          }
        }
      } );

      // Hack alert : more XUL limitations...
      //
      ToolItem sep = new ToolItem( swtToolbar, SWT.SEPARATOR );

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

      zoomLabel.addKeyListener( new KeyAdapter() {
        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( Const.getStackTracker( t ) );
      new ErrorDialog( shell,
        BaseMessages.getString( PKG, "Spoon.Exception.ErrorReadingXULFile.Title" ),
        BaseMessages.getString( PKG, "Spoon.Exception.ErrorReadingXULFile.Message", XUL_FILE_JOB_GRAPH ),
        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 ) {
      MessageBox mb = new MessageBox( shell, SWT.YES | SWT.ICON_ERROR );
      mb.setMessage( BaseMessages.getString( PKG, "TransGraph.Dialog.InvalidZoomMeasurement.Message", zoomLabel
        .getText() ) );
      mb.setText( BaseMessages.getString( PKG, "TransGraph.Dialog.InvalidZoomMeasurement.Title" ) );
      mb.open();
    }
    redraw();
  }

  public void keyPressed( KeyEvent e ) {

    // Delete
    if ( e.keyCode == SWT.DEL ) {
      List<JobEntryCopy> copies = jobMeta.getSelectedEntries();
      if ( copies != null && copies.size() > 0 ) {
        delSelected();
      }
    }

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

    // 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 );
    }
    // 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();
    }
  }

  public void keyReleased( KeyEvent e ) {
  }

  public void selectInRect( JobMeta jobMeta, org.pentaho.di.core.gui.Rectangle rect ) {
    int i;
    for ( i = 0; i < jobMeta.nrJobEntries(); i++ ) {
      JobEntryCopy je = jobMeta.getJobEntry( i );
      Point p = je.getLocation();
      if ( ( ( p.x >= rect.x && p.x <= rect.x + rect.width ) || ( p.x >= rect.x + rect.width && p.x <= rect.x ) )
        && ( ( p.y >= rect.y && p.y <= rect.y + rect.height ) || ( p.y >= rect.y + rect.height && p.y <= rect.y ) ) ) {
        je.setSelected( true );
      }
    }
    for ( i = 0; i < jobMeta.nrNotes(); i++ ) {
      NotePadMeta ni = jobMeta.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 );
      }
    }
  }

  public boolean setFocus() {
    xulDomContainer.addEventHandler( this );
    return canvas.setFocus();
  }

  /**
   * Method gets called, when the user wants to change a job entries name and he indeed entered a different name then
   * the old one. Make sure that no other job entry matches this name and rename in case of uniqueness.
   *
   * @param jobEntry
   * @param newName
   */
  public void renameJobEntry( JobEntryCopy jobEntry, String newName ) {
    JobEntryCopy[] jobs = jobMeta.getAllJobGraphEntries( newName );
    if ( jobs != null && jobs.length > 0 ) {
      MessageBox mb = new MessageBox( shell, SWT.OK | SWT.ICON_INFORMATION );
      mb.setMessage( BaseMessages.getString( PKG, "Spoon.Dialog.JobEntryNameExists.Message", newName ) );
      mb.setText( BaseMessages.getString( PKG, "Spoon.Dialog.JobEntryNameExists.Title" ) );
      mb.open();
    } else {
      jobEntry.setName( newName );
      jobEntry.setChanged();
      spoon.refreshTree(); // to reflect the new name
      spoon.refreshGraph();
    }
  }

  public static void showOnlyStartOnceMessage( Shell shell ) {
    MessageBox mb = new MessageBox( shell, SWT.YES | SWT.ICON_ERROR );
    mb.setMessage( BaseMessages.getString( PKG, "JobGraph.Dialog.OnlyUseStartOnce.Message" ) );
    mb.setText( BaseMessages.getString( PKG, "JobGraph.Dialog.OnlyUseStartOnce.Title" ) );
    mb.open();
  }

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

  public void delSelected( JobEntryCopy clickedEntry ) {
    List<JobEntryCopy> copies = jobMeta.getSelectedEntries();
    int nrsels = copies.size();
    if ( nrsels == 0 ) {
      if ( clickedEntry != null ) {
        spoon.deleteJobEntryCopies( jobMeta, clickedEntry );
      }
      return;
    }

    JobEntryCopy[] jobEntries = copies.toArray( new JobEntryCopy[copies.size()] );
    spoon.deleteJobEntryCopies( jobMeta, jobEntries );
  }

  public void clearSettings() {
    selectedEntry = null;
    selectedNote = null;
    selectedEntries = null;
    selectedNotes = null;
    selectionRegion = null;
    hop_candidate = null;
    last_hop_split = null;
    lastButton = 0;
    startHopEntry = null;
    endHopEntry = null;
    iconoffset = null;
    for ( int i = 0; i < jobMeta.nrJobHops(); i++ ) {
      jobMeta.getJobHop( i ).setSplit( false );
    }

    stopEntryMouseOverDelayTimers();
  }

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

    p.x = x - p.x - 8;
    p.y = y - p.y - 48;

    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
   */
  private JobHopMeta findJobHop( 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 JobHopMeta findHop( int x, int y, JobEntryCopy exclude ) {
    int i;
    JobHopMeta online = null;
    for ( i = 0; i < jobMeta.nrJobHops(); i++ ) {
      JobHopMeta hi = jobMeta.getJobHop( i );
      JobEntryCopy fs = hi.getFromEntry();
      JobEntryCopy ts = hi.getToEntry();

      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;
  }

  protected int[] getLine( JobEntryCopy fs, JobEntryCopy ts ) {
    if ( fs == null || ts == null ) {
      return null;
    }

    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 };
  }

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

    helpTip.setTitle( tipTitle );
    helpTip.setMessage( tipMessage );
    helpTip.setCheckBoxMessage( BaseMessages.getString(
      PKG, "JobGraph.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 );
  }

  public void setJobEntry( JobEntryCopy jobEntry ) {
    this.jobEntry = jobEntry;
  }

  public JobEntryCopy getJobEntry() {
    return jobEntry;
  }

  public void openTransformation() {
    JobEntryCopy jobEntryCopy = getJobEntry();
    final JobEntryInterface entry = jobEntryCopy.getEntry();
    openTransformation( (JobEntryTrans) entry, jobEntryCopy );
  }

  public void openJob() {
    JobEntryCopy jobEntryCopy = getJobEntry();
    final JobEntryInterface entry = jobEntryCopy.getEntry();
    openJob( (JobEntryJob) entry, jobEntryCopy );
  }

  public void newHopClick() {
    selectedEntries = null;
    newHop();
  }

  public void editEntryClick() {
    selectedEntries = null;
    editEntry( getJobEntry() );
  }

  public void editEntryDescription() {
    String title = BaseMessages.getString( PKG, "JobGraph.Dialog.EditDescription.Title" );
    String message = BaseMessages.getString( PKG, "JobGraph.Dialog.EditDescription.Message" );
    EnterTextDialog dd = new EnterTextDialog( shell, title, message, getJobEntry().getDescription() );
    String des = dd.open();
    if ( des != null ) {
      jobEntry.setDescription( des );
      jobEntry.setChanged();
      spoon.setShellText();
    }
  }

  /**
   * Go from serial to parallel to serial execution
   */
  public void editEntryParallel() {

    JobEntryCopy je = getJobEntry();
    JobEntryCopy jeOld = (JobEntryCopy) je.clone_deep();

    je.setLaunchingInParallel( !je.isLaunchingInParallel() );
    JobEntryCopy jeNew = (JobEntryCopy) je.clone_deep();

    spoon.addUndoChange( jobMeta, new JobEntryCopy[] { jeOld }, new JobEntryCopy[] { jeNew }, new int[] { jobMeta
      .indexOfJobEntry( jeNew ) } );
    jobMeta.setChanged();

    if ( getJobEntry().isLaunchingInParallel() ) {
      // Show a warning (optional)
      //
      if ( "Y".equalsIgnoreCase( spoon.props.getCustomParameter( STRING_PARALLEL_WARNING_PARAMETER, "Y" ) ) ) {
        MessageDialogWithToggle md =
          new MessageDialogWithToggle( shell,
            BaseMessages.getString( PKG, "JobGraph.ParallelJobEntriesWarning.DialogTitle" ),
            null,
            BaseMessages.getString( PKG, "JobGraph.ParallelJobEntriesWarning.DialogMessage", Const.CR ) + Const.CR,
            MessageDialog.WARNING,
            new String[] { BaseMessages.getString( PKG, "JobGraph.ParallelJobEntriesWarning.Option1" ) },
            0,
            BaseMessages.getString( PKG, "JobGraph.ParallelJobEntriesWarning.Option2" ),
            "N".equalsIgnoreCase( spoon.props.getCustomParameter( STRING_PARALLEL_WARNING_PARAMETER, "Y" ) ) );
        MessageDialogWithToggle.setDefaultImage( GUIResource.getInstance().getImageSpoon() );
        md.open();
        spoon.props.setCustomParameter( STRING_PARALLEL_WARNING_PARAMETER, md.getToggleState() ? "N" : "Y" );
        spoon.props.saveProps();
      }
    }
    spoon.refreshGraph();

  }

  public void duplicateEntry() throws KettleException {
    if ( !canDup( jobEntry ) ) {
      JobGraph.showOnlyStartOnceMessage( spoon.getShell() );
    }

    spoon.delegates.jobs.dupeJobEntry( jobMeta, jobEntry );
  }

  public void copyEntry() {
    if ( RepositorySecurityUI.verifyOperations(
        shell, spoon.rep, RepositoryOperation.MODIFY_JOB, RepositoryOperation.EXECUTE_JOB ) ) {
      return;
    }
    List<JobEntryCopy> entries = jobMeta.getSelectedEntries();
    Iterator<JobEntryCopy> iterator = entries.iterator();
    while ( iterator.hasNext() ) {
      JobEntryCopy entry = iterator.next();
      if ( !canDup( entry ) ) {
        iterator.remove();
      }
    }

    spoon.delegates.jobs.copyJobEntries( jobMeta, entries );
  }

  private boolean canDup( JobEntryCopy entry ) {
    return !entry.isStart();
  }

  public void detachEntry() {
    detach( getJobEntry() );
    jobMeta.unselectAll();
  }

  public void hideEntry() {
    getJobEntry().setDrawn( false );
    // nr > 1: delete
    if ( jobEntry.getNr() > 0 ) {
      int ind = jobMeta.indexOfJobEntry( jobEntry );
      jobMeta.removeJobEntry( ind );
      spoon.addUndoDelete( jobMeta, new JobEntryCopy[] { getJobEntry() }, new int[] { ind } );
    }
    redraw();
  }

  public void deleteEntry() {
    delSelected();
    redraw();
  }

  protected synchronized void setMenu( int x, int y ) {

    currentMouseX = x;
    currentMouseY = y;

    final JobEntryCopy jobEntry = jobMeta.getJobEntryCopy( x, y, iconsize );
    setJobEntry( jobEntry );
    Document doc = xulDomContainer.getDocumentRoot();
    if ( jobEntry != null ) {
      // We clicked on a Job Entry!

      XulMenupopup menu = (XulMenupopup) doc.getElementById( "job-graph-entry" );
      if ( menu != null ) {
        List<JobEntryCopy> selection = jobMeta.getSelectedEntries();
        doRightClickSelection( jobEntry, selection );
        int sels = selection.size();

        XulMenuitem item = (XulMenuitem) doc.getElementById( "job-graph-entry-newhop" );
        item.setDisabled( sels < 2 );

        JfaceMenupopup launchMenu = (JfaceMenupopup) doc.getElementById( "job-graph-entry-launch-popup" );
        String[] referencedObjects = jobEntry.getEntry().getReferencedObjectDescriptions();
        boolean[] enabledObjects = jobEntry.getEntry().isReferencedObjectEnabled();
        launchMenu.setDisabled( Const.isEmpty( referencedObjects ) );

        launchMenu.removeChildren();

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

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

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

        XulMenu aMenu = (XulMenu) doc.getElementById( "job-graph-entry-align" );
        if ( aMenu != null ) {
          aMenu.setDisabled( sels < 1 );
        }

        item = (XulMenuitem) doc.getElementById( "job-graph-entry-detach" );
        if ( item != null ) {
          item.setDisabled( !jobMeta.isEntryUsedInHops( jobEntry ) );
        }

        item = (XulMenuitem) doc.getElementById( "job-graph-entry-hide" );
        if ( item != null ) {
          item.setDisabled( !( jobEntry.isDrawn() && !jobMeta.isEntryUsedInHops( jobEntry ) ) );
        }

        item = (XulMenuitem) doc.getElementById( "job-graph-entry-delete" );
        if ( item != null ) {
          item.setDisabled( !jobEntry.isDrawn() );
        }

        item = (XulMenuitem) doc.getElementById( "job-graph-entry-parallel" );
        if ( item != null ) {
          item.setSelected( jobEntry.isLaunchingInParallel() );
        }

        try {
          JobGraphJobEntryMenuExtension extension =
            new JobGraphJobEntryMenuExtension( xulDomContainer, doc, jobMeta, jobEntry, this );
          ExtensionPointHandler.callExtensionPoint(
            log, KettleExtensionPoint.JobGraphJobEntrySetMenu.id, extension );
        } catch ( Exception e ) {
          log.logError( "Error handling menu right click on job entry through extension point", e );
        }

        ConstUI.displayMenu( menu, canvas );
      }

    } else {
      // Clear the menu

      final JobHopMeta hi = findJobHop( x, y );
      setCurrentHop( hi );

      if ( hi != null ) {
        // We clicked on a HOP!

        XulMenupopup menu = (XulMenupopup) doc.getElementById( "job-graph-hop" );
        if ( menu != null ) {
          XulMenuitem miPopEvalUncond = (XulMenuitem) doc.getElementById( "job-graph-hop-evaluation-uncond" );
          XulMenuitem miPopEvalTrue = (XulMenuitem) doc.getElementById( "job-graph-hop-evaluation-true" );
          XulMenuitem miPopEvalFalse = (XulMenuitem) doc.getElementById( "job-graph-hop-evaluation-false" );
          XulMenuitem miDisHop = (XulMenuitem) doc.getElementById( "job-graph-hop-enabled" );
          XulMenuitem miFlipHop = (XulMenuitem) doc.getElementById( "job-graph-hop-flip" );

          // Set the checkboxes in the right places...
          //
          if ( miPopEvalUncond != null && miPopEvalTrue != null && miPopEvalFalse != null ) {
            if ( hi.isUnconditional() ) {
              miPopEvalUncond.setSelected( true );
              miPopEvalTrue.setSelected( false );
              miPopEvalFalse.setSelected( false );
            } else {
              if ( hi.getEvaluation() ) {
                miPopEvalUncond.setSelected( false );
                miPopEvalTrue.setSelected( true );
                miPopEvalFalse.setSelected( false );
              } else {
                miPopEvalUncond.setSelected( false );
                miPopEvalTrue.setSelected( false );
                miPopEvalFalse.setSelected( true );
              }
            }
            if ( !hi.getFromEntry().evaluates() ) {
              miPopEvalTrue.setDisabled( true );
              miPopEvalFalse.setDisabled( true );
            } else {
              miPopEvalTrue.setDisabled( false );
              miPopEvalFalse.setDisabled( false );
            }
            if ( !hi.getFromEntry().isUnconditional() ) {
              miPopEvalUncond.setDisabled( true );
            } else {
              miPopEvalUncond.setDisabled( false );
            }
            if ( hi.getFromEntry().isStart() || hi.getToEntry().getEntry() instanceof JobEntryAbort ) {
              miFlipHop.setDisabled( true );
            } else {
              miFlipHop.setDisabled( false );
            }
          }

          if ( miDisHop != null ) {
            if ( hi.isEnabled() ) {
              miDisHop.setLabel( BaseMessages.getString( PKG, "JobGraph.PopupMenu.Hop.Disable" ) );
            } else {
              miDisHop.setLabel( BaseMessages.getString( PKG, "JobGraph.PopupMenu.Hop.Enable" ) );
            }
          }
          ConstUI.displayMenu( menu, canvas );
        }

      } else {
        // Clicked on the background: maybe we hit a note?
        final NotePadMeta ni = jobMeta.getNote( x, y );
        setCurrentNote( ni );
        if ( ni != null ) {
          XulMenupopup menu = (XulMenupopup) doc.getElementById( "job-graph-note" );
          if ( menu != null ) {
            ConstUI.displayMenu( menu, canvas );
          }
        } else {
          XulMenupopup menu = (XulMenupopup) doc.getElementById( "job-graph-background" );
          if ( menu != null ) {
            final String clipcontent = spoon.fromClipboard();
            XulMenuitem item = (XulMenuitem) doc.getElementById( "job-graph-note-paste" );
            if ( item != null ) {
              item.setDisabled( clipcontent == null );
            }

            ConstUI.displayMenu( menu, canvas );
          }
        }
      }
    }
  }

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

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

  public void editJobProperties() {
    editProperties( jobMeta, spoon, spoon.getRepository(), true );
  }

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

  public void newNote() {
    String title = BaseMessages.getString( PKG, "JobGraph.Dialog.EditNote.Title" );
    String message = BaseMessages.getString( PKG, "JobGraph.Dialog.EditNote.Message" );
    EnterTextDialog dd = new EnterTextDialog( shell, title, message, "" );
    String n = dd.open();
    if ( n != null ) {
      NotePadMeta npi =
        new NotePadMeta( n, lastclick.x, lastclick.y, ConstUI.NOTE_MIN_SIZE, ConstUI.NOTE_MIN_SIZE );
      jobMeta.addNote( npi );
      spoon.addUndoNew( jobMeta, new NotePadMeta[] { npi }, new int[] { jobMeta.indexOfNote( npi ) } );
      redraw();
    }
  }

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

  public NotePadMeta getCurrentNote() {
    return ni;
  }

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

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

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

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

  public void flipHop() {
    selectionRegion = null;
    JobEntryCopy origFrom = currentHop.getFromEntry();
    JobEntryCopy origTo = currentHop.getToEntry();
    currentHop.setFromEntry( currentHop.getToEntry() );
    currentHop.setToEntry( origFrom );

    boolean cancel = false;
    if ( jobMeta.hasLoop( currentHop.getFromEntry() ) || jobMeta.hasLoop( currentHop.getToEntry() ) ) {
      MessageBox mb = new MessageBox( shell, SWT.OK | SWT.CANCEL | SWT.ICON_WARNING );
      mb.setMessage( BaseMessages.getString( PKG, "JobGraph.Dialog.HopFlipCausesLoop.Message" ) );
      mb.setText( BaseMessages.getString( PKG, "JobGraph.Dialog.HopCausesLoop.Title" ) );
      int choice = mb.open();
      if ( choice == SWT.CANCEL ) {
        cancel = true;
        currentHop.setFromEntry( origFrom );
        currentHop.setToEntry( origTo );
      }
    }
    if ( !cancel ) {
      currentHop.setChanged();
    }
    spoon.refreshGraph();
    spoon.refreshTree();
    spoon.setShellText();
  }

  public void disableHop() {
    selectionRegion = null;
    boolean orig = currentHop.isEnabled();
    currentHop.setEnabled( !currentHop.isEnabled() );

    if ( !orig && ( jobMeta.hasLoop( currentHop.getFromEntry() ) || jobMeta.hasLoop( currentHop.getToEntry() ) ) ) {
      MessageBox mb = new MessageBox( shell, SWT.CANCEL | SWT.OK | SWT.ICON_WARNING );
      mb.setMessage( BaseMessages.getString( PKG, "JobGraph.Dialog.LoopAfterHopEnabled.Message" ) );
      mb.setText( BaseMessages.getString( PKG, "JobGraph.Dialog.LoopAfterHopEnabled.Title" ) );
      int choice = mb.open();
      if ( choice == SWT.CANCEL ) {
        currentHop.setEnabled( orig );
      }
    }
    spoon.refreshGraph();
    spoon.refreshTree();
  }

  public void deleteHop() {
    selectionRegion = null;
    int idx = jobMeta.indexOfJobHop( currentHop );
    jobMeta.removeJobHop( idx );
    spoon.refreshTree();
    spoon.refreshGraph();
  }

  public void setHopUnconditional() {
    currentHop.setUnconditional();
    spoon.refreshGraph();
  }

  public void setHopEvaluationTrue() {
    currentHop.setConditional();
    currentHop.setEvaluation( true );
    spoon.refreshGraph();
  }

  public void setHopEvaluationFalse() {
    currentHop.setConditional();
    currentHop.setEvaluation( false );
    spoon.refreshGraph();
  }

  protected void setCurrentHop( JobHopMeta hop ) {
    currentHop = hop;
  }

  protected JobHopMeta getCurrentHop() {
    return currentHop;
  }

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

  public void disableHopsBetweenSelectedEntries() {
    enableHopsBetweenSelectedEntries( false );
  }

  /**
   * This method enables or disables all the hops between the selected Entries.
   *
   **/
  public void enableHopsBetweenSelectedEntries( boolean enabled ) {
    List<JobEntryCopy> list = jobMeta.getSelectedEntries();

    boolean hasLoop = false;

    for ( int i = 0; i < jobMeta.nrJobHops(); i++ ) {
      JobHopMeta hop = jobMeta.getJobHop( i );
      if ( list.contains( hop.getFromEntry() ) && list.contains( hop.getToEntry() ) ) {

        JobHopMeta before = (JobHopMeta) hop.clone();
        hop.setEnabled( enabled );
        JobHopMeta after = (JobHopMeta) hop.clone();
        spoon.addUndoChange( jobMeta, new JobHopMeta[] { before }, new JobHopMeta[] { after }, new int[] { jobMeta
          .indexOfJobHop( hop ) } );
        if ( jobMeta.hasLoop( hop.getFromEntry() ) || jobMeta.hasLoop( hop.getToEntry() ) ) {
          hasLoop = true;
        }
      }
    }

    if ( hasLoop && enabled ) {
      MessageBox mb = new MessageBox( shell, SWT.OK | SWT.ICON_WARNING );
      mb.setMessage( BaseMessages.getString( PKG, "JobGraph.Dialog.LoopAfterHopEnabled.Message" ) );
      mb.setText( BaseMessages.getString( PKG, "JobGraph.Dialog.LoopAfterHopEnabled.Title" ) );
      mb.open();
    }

    spoon.refreshGraph();
  }

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

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

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

    JobHopMeta before = (JobHopMeta) currentHop.clone();
    currentHop.setEnabled( enabled );
    JobHopMeta after = (JobHopMeta) currentHop.clone();
    spoon.addUndoChange( jobMeta, new JobHopMeta[] { before }, new JobHopMeta[] { after }, new int[] { jobMeta
      .indexOfJobHop( currentHop ) } );

    enableDisableNextHops( currentHop.getToEntry(), enabled, 1 );

    spoon.refreshGraph();
  }

  private void enableDisableNextHops( JobEntryCopy from, boolean enabled, int level ) {

    if ( level > 100 ) {
      return; // prevent endless running with loops in jobs
    }

    for ( JobEntryCopy to : jobMeta.getJobCopies() ) {
      JobHopMeta hop = jobMeta.findJobHop( from, to, true );
      if ( hop != null ) {
        JobHopMeta before = (JobHopMeta) hop.clone();
        hop.setEnabled( enabled );
        JobHopMeta after = (JobHopMeta) hop.clone();
        spoon.addUndoChange( jobMeta, new JobHopMeta[] { before }, new JobHopMeta[] { after }, new int[] { jobMeta
          .indexOfJobHop( hop ) } );

        enableDisableNextHops( to, enabled, level++ );
      }
    }
  }

  protected void setToolTip( int x, int y, int screenX, int screenY ) {
    if ( !spoon.getProperties().showToolTips() ) {
      return;
    }

    canvas.setToolTipText( "-" ); // Some stupid bug in GTK+ causes a phantom tool tip to pop up, even if the tip is
                                  // null
    canvas.setToolTipText( null );

    Image tipImage = null;

    JobHopMeta hi = findJobHop( x, y );

    // check the area owner list...
    //
    StringBuilder tip = new StringBuilder();
    AreaOwner areaOwner = getVisibleAreaOwner( x, y );
    if ( areaOwner != null ) {
      JobEntryCopy jobEntryCopy;
      switch ( areaOwner.getAreaType() ) {
        case JOB_HOP_ICON:
          hi = (JobHopMeta) areaOwner.getOwner();
          if ( hi.isUnconditional() ) {
            tipImage = GUIResource.getInstance().getImageUnconditionalHop();
            tip.append( BaseMessages.getString( PKG, "JobGraph.Hop.Tooltip.Unconditional", hi
              .getFromEntry().getName(), Const.CR ) );
          } else {
            if ( hi.getEvaluation() ) {
              tip.append( BaseMessages.getString( PKG, "JobGraph.Hop.Tooltip.EvaluatingTrue", hi
                .getFromEntry().getName(), Const.CR ) );
              tipImage = GUIResource.getInstance().getImageTrue();
            } else {
              tip.append( BaseMessages.getString( PKG, "JobGraph.Hop.Tooltip.EvaluatingFalse", hi
                .getFromEntry().getName(), Const.CR ) );
              tipImage = GUIResource.getInstance().getImageFalse();
            }
          }
          break;

        case JOB_HOP_PARALLEL_ICON:
          hi = (JobHopMeta) areaOwner.getOwner();
          tip.append( BaseMessages.getString(
            PKG, "JobGraph.Hop.Tooltip.Parallel", hi.getFromEntry().getName(), Const.CR ) );
          tipImage = GUIResource.getInstance().getImageParallelHop();
          break;

        case CUSTOM:
          String message = (String) areaOwner.getOwner();
          tip.append( message );
          tipImage = null;
          GUIResource.getInstance().getImageTransGraph();
          break;

        case JOB_ENTRY_MINI_ICON_INPUT:
          tip.append( BaseMessages.getString( PKG, "JobGraph.EntryInputConnector.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageHopInput();
          resetDelayTimer( (JobEntryCopy) areaOwner.getOwner() );
          break;

        case JOB_ENTRY_MINI_ICON_OUTPUT:
          tip.append( BaseMessages.getString( PKG, "JobGraph.EntryOutputConnector.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageHopOutput();
          resetDelayTimer( (JobEntryCopy) areaOwner.getOwner() );
          break;

        case JOB_ENTRY_MINI_ICON_EDIT:
          tip.append( BaseMessages.getString( PKG, "JobGraph.EditStep.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageEdit();
          resetDelayTimer( (JobEntryCopy) areaOwner.getOwner() );
          break;

        case JOB_ENTRY_MINI_ICON_CONTEXT:
          tip.append( BaseMessages.getString( PKG, "JobGraph.ShowMenu.Tooltip" ) );
          tipImage = GUIResource.getInstance().getImageContextMenu();
          resetDelayTimer( (JobEntryCopy) areaOwner.getOwner() );
          break;

        case JOB_ENTRY_RESULT_FAILURE:
        case JOB_ENTRY_RESULT_SUCCESS:
          JobEntryResult jobEntryResult = (JobEntryResult) areaOwner.getOwner();
          jobEntryCopy = (JobEntryCopy) areaOwner.getParent();
          Result result = jobEntryResult.getResult();
          tip.append( "'" ).append( jobEntryCopy.getName() ).append( "' " );
          if ( result.getResult() ) {
            tipImage = GUIResource.getInstance().getImageTrue();
            tip.append( "finished successfully." );
          } else {
            tipImage = GUIResource.getInstance().getImageFalse();
            tip.append( "failed." );
          }
          tip.append( Const.CR ).append( "------------------------" ).append( Const.CR ).append( Const.CR );
          tip.append( "Result         : " ).append( result.getResult() ).append( Const.CR );
          tip.append( "Errors         : " ).append( result.getNrErrors() ).append( Const.CR );

          if ( result.getNrLinesRead() > 0 ) {
            tip.append( "Lines read     : " ).append( result.getNrLinesRead() ).append( Const.CR );
          }
          if ( result.getNrLinesWritten() > 0 ) {
            tip.append( "Lines written  : " ).append( result.getNrLinesWritten() ).append( Const.CR );
          }
          if ( result.getNrLinesInput() > 0 ) {
            tip.append( "Lines input    : " ).append( result.getNrLinesInput() ).append( Const.CR );
          }
          if ( result.getNrLinesOutput() > 0 ) {
            tip.append( "Lines output   : " ).append( result.getNrLinesOutput() ).append( Const.CR );
          }
          if ( result.getNrLinesUpdated() > 0 ) {
            tip.append( "Lines updated  : " ).append( result.getNrLinesUpdated() ).append( Const.CR );
          }
          if ( result.getNrLinesDeleted() > 0 ) {
            tip.append( "Lines deleted  : " ).append( result.getNrLinesDeleted() ).append( Const.CR );
          }
          if ( result.getNrLinesRejected() > 0 ) {
            tip.append( "Lines rejected : " ).append( result.getNrLinesRejected() ).append( Const.CR );
          }
          if ( result.getResultFiles() != null && !result.getResultFiles().isEmpty() ) {
            tip.append( Const.CR ).append( "Result files:" ).append( Const.CR );
            if ( result.getResultFiles().size() > 10 ) {
              tip.append( " (10 files of " ).append( result.getResultFiles().size() ).append( " shown" );
            }
            List<ResultFile> files = new ArrayList<ResultFile>( result.getResultFiles().values() );
            for ( int i = 0; i < files.size(); i++ ) {
              ResultFile file = files.get( i );
              tip.append( "  - " ).append( file.toString() ).append( Const.CR );
            }
          }
          if ( result.getRows() != null && !result.getRows().isEmpty() ) {
            tip.append( Const.CR ).append( "Result rows: " );
            if ( result.getRows().size() > 10 ) {
              tip.append( " (10 rows of " ).append( result.getRows().size() ).append( " shown" );
            }
            tip.append( Const.CR );
            for ( int i = 0; i < result.getRows().size() && i < 10; i++ ) {
              RowMetaAndData row = result.getRows().get( i );
              tip.append( "  - " ).append( row.toString() ).append( Const.CR );
            }
          }
          break;

        case JOB_ENTRY_RESULT_CHECKPOINT:
          tip.append( "The job started here since this is the furthest checkpoint "
            + "that was reached last time the transformation was executed." );
          tipImage = GUIResource.getInstance().getImageCheckpoint();
          break;
        default:
          break;
      }
    }

    if ( hi != null && tip.length() == 0 ) {
      // Set the tooltip for the hop:
      tip.append( BaseMessages.getString( PKG, "JobGraph.Dialog.HopInfo" ) ).append( Const.CR );
      tip.append( BaseMessages.getString( PKG, "JobGraph.Dialog.HopInfo.SourceEntry" ) ).append( " " ).append(
        hi.getFromEntry().getName() ).append( Const.CR );
      tip.append( BaseMessages.getString( PKG, "JobGraph.Dialog.HopInfo.TargetEntry" ) ).append( " " ).append(
        hi.getToEntry().getName() ).append( Const.CR );
      tip.append( BaseMessages.getString( PKG, "TransGraph.Dialog.HopInfo.Status" ) ).append( " " );
      tip.append( ( hi.isEnabled()
        ? BaseMessages.getString( PKG, "JobGraph.Dialog.HopInfo.Enable" ) : BaseMessages.getString(
          PKG, "JobGraph.Dialog.HopInfo.Disable" ) ) );
      if ( hi.isUnconditional() ) {
        tipImage = GUIResource.getInstance().getImageUnconditionalHop();
      } else {
        if ( hi.getEvaluation() ) {
          tipImage = GUIResource.getInstance().getImageTrue();
        } else {
          tipImage = GUIResource.getInstance().getImageFalse();
        }
      }
    }

    if ( tip == null || tip.length() == 0 ) {
      toolTip.hide();
    } else {
      if ( !tip.toString().equalsIgnoreCase( getToolTipText() ) ) {
        if ( tipImage != null ) {
          toolTip.setImage( tipImage );
        } else {
          toolTip.setImage( GUIResource.getInstance().getImageSpoon() );
        }
        toolTip.setText( tip.toString() );
        toolTip.hide();
        toolTip.show( new org.eclipse.swt.graphics.Point( screenX, screenY ) );
      }
    }
  }

  public void launchStuff( JobEntryCopy jobEntryCopy ) {
    String[] references = jobEntryCopy.getEntry().getReferencedObjectDescriptions();
    if ( !Const.isEmpty( references ) ) {
      loadReferencedObject( jobEntryCopy, 0 );
    }

    /*
     * if (jobEntryCopy.isJob()) { final JobEntryJob entry = (JobEntryJob) jobEntryCopy.getEntry(); if ((entry != null
     * && entry.getJobObjectId() == null && !Const.isEmpty(entry.getFilename()) && spoon.rep == null) || (entry != null
     * && !Const.isEmpty(entry.getName()) && spoon.rep != null) || (entry != null && entry.getJobObjectId()!=null &&
     * spoon.rep != null) ) { openJob(entry, jobEntryCopy); } } else if (jobEntryCopy.isTransformation()) { final
     * JobEntryTrans entry = (JobEntryTrans) jobEntryCopy.getEntry(); if ((entry != null && entry.getTransObjectId() ==
     * null && !Const.isEmpty(entry.getFilename()) && spoon.rep == null) || (entry != null && entry.getName() != null &&
     * spoon.rep != null) || (entry != null && entry.getTransObjectId()!=null && spoon.rep != null) ) {
     * openTransformation(entry, jobEntryCopy); } }
     */
  }

  public void launchStuff() {
    if ( jobEntry != null ) {
      launchStuff( jobEntry );
    }
  }

  protected void loadReferencedObject( JobEntryCopy jobEntryCopy, int index ) {
    try {
      Object referencedMeta =
        jobEntryCopy.getEntry().loadReferencedObject( index, spoon.rep, spoon.metaStore, jobMeta );
      if ( referencedMeta == null ) {
        // Compatible re-try for older plugins.
        referencedMeta =
          compatibleJobEntryLoadReferencedObject( jobEntryCopy.getEntry(), index, spoon.rep, jobMeta );
      }
      if ( referencedMeta != null && ( referencedMeta instanceof TransMeta ) ) {
        TransMeta launchTransMeta = (TransMeta) referencedMeta;

        // Try to see if this transformation is already loaded in another tab...
        //
        TabMapEntry tabEntry = spoon.delegates.tabs.findTabForTransformation( launchTransMeta );
        if ( tabEntry != null ) {
          // Switch to this one!
          //
          spoon.tabfolder.setSelected( tabEntry.getTabItem() );
          return;
        }

        copyInternalJobVariables( jobMeta, launchTransMeta );
        spoon.setParametersAsVariablesInUI( launchTransMeta, launchTransMeta );

        launchTransMeta.clearChanged();
        spoon.addTransGraph( launchTransMeta );

        TransGraph transGraph = spoon.getActiveTransGraph();
        attachActiveTrans( transGraph, launchTransMeta, jobEntryCopy );

        spoon.refreshTree();
        spoon.applyVariables();
      }

      if ( referencedMeta != null && ( referencedMeta instanceof JobMeta ) ) {
        JobMeta launchJobMeta = (JobMeta) referencedMeta;

        // Try to see if this job is already loaded in another tab...
        //
        String tabName = spoon.delegates.tabs.makeTabName( launchJobMeta, true );
        TabMapEntry tabEntry = spoon.delegates.tabs.findTabMapEntry( tabName, ObjectType.JOB_GRAPH );
        if ( tabEntry != null ) {
          // Switch to this one!
          //
          spoon.tabfolder.setSelected( tabEntry.getTabItem() );
          return;
        }

        spoon.setParametersAsVariablesInUI( launchJobMeta, launchJobMeta );

        launchJobMeta.clearChanged();
        spoon.addJobGraph( launchJobMeta );

        JobGraph jobGraph = spoon.getActiveJobGraph();
        attachActiveJob( jobGraph, launchJobMeta, jobEntryCopy );

        spoon.refreshTree();
        spoon.applyVariables();
      }
    } catch ( Exception e ) {
      new ErrorDialog( shell,
        BaseMessages.getString( PKG, "JobGraph.Dialog.ErrorLaunchingSpoonCanNotLoadTransformation.Title" ),
        BaseMessages.getString( PKG, "JobGraph.Dialog.ErrorLaunchingSpoonCanNotLoadTransformation.Message" ), e );
    }
  }

  @SuppressWarnings( "deprecation" )
  private Object compatibleJobEntryLoadReferencedObject( JobEntryInterface entry, int index, Repository rep,
    JobMeta jobMeta2 ) throws KettleException {
    return entry.loadReferencedObject( index, spoon.rep, jobMeta );
  }

  protected void openTransformation( JobEntryTrans entry, JobEntryCopy jobEntryCopy ) {

    try {

      TransMeta launchTransMeta = null;

      switch ( entry.getSpecificationMethod() ) {
        case FILENAME:
          // See if this file is already loaded...
          //
          String exactFilename = jobMeta.environmentSubstitute( entry.getFilename() );
          if ( Const.isEmpty( exactFilename ) ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoFilenameSpecified" ) );
          }

          // Open the file or create a new one!
          //
          if ( KettleVFS.fileExists( exactFilename ) ) {
            launchTransMeta = new TransMeta( exactFilename );
          } else {
            launchTransMeta = new TransMeta();
          }
          launchTransMeta.setFilename( exactFilename );
          break;

        case REPOSITORY_BY_NAME:
          String exactTransname = jobMeta.environmentSubstitute( entry.getTransname() );
          String exactDirectory = jobMeta.environmentSubstitute( entry.getDirectory() );
          if ( Const.isEmpty( exactTransname ) ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoTransNameSpecified" ) );
          }
          if ( Const.isEmpty( exactDirectory ) ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoTransDirectorySpecified" ) );
          }

          // Open the transformation or create a new one...

          // But first we look to see if the directory does exist
          RepositoryDirectoryInterface repositoryDirectoryInterface =
            spoon.rep.findDirectory( jobMeta.environmentSubstitute( entry.getDirectory() ) );
          if ( repositoryDirectoryInterface == null ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.DirectoryDoesNotExist", jobMeta
              .environmentSubstitute( entry.getDirectory() ) ) );
          }

          boolean exists = spoon.rep.getTransformationID( exactTransname, repositoryDirectoryInterface ) != null;
          if ( !exists ) {
            launchTransMeta = new TransMeta( null, exactTransname );
          } else {
            launchTransMeta =
              spoon.rep.loadTransformation( exactTransname, spoon.rep.findDirectory( jobMeta
                .environmentSubstitute( entry.getDirectory() ) ), null, true, null ); // reads last version
          }
          break;
        case REPOSITORY_BY_REFERENCE:
          if ( entry.getTransObjectId() == null ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoTransReferenceSpecified" ) );
          }
          launchTransMeta = spoon.rep.loadTransformation( entry.getTransObjectId(), null );
          break;
        default:
          break;
      }

      // If we didn't find a valid transformation, stop here...
      //
      if ( launchTransMeta == null ) {
        throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoValidTransSpecified" ) );
      }

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

      // Try to see if this transformation is already loaded in another tab...
      //
      TabMapEntry tabEntry = spoon.delegates.tabs.findTabForTransformation( launchTransMeta );
      if ( tabEntry != null ) {
        // Switch to this one!
        //
        spoon.tabfolder.setSelected( tabEntry.getTabItem() );
        return;
      }

      copyInternalJobVariables( jobMeta, launchTransMeta );
      spoon.setParametersAsVariablesInUI( launchTransMeta, launchTransMeta );

      spoon.addTransGraph( launchTransMeta );
      launchTransMeta.clearChanged();

      TransGraph transGraph = spoon.getActiveTransGraph();
      attachActiveTrans( transGraph, launchTransMeta, jobEntryCopy );

      spoon.refreshTree();
      spoon.applyVariables();

    } catch ( Throwable e ) {
      new ErrorDialog( shell,
        BaseMessages.getString( PKG, "JobGraph.Dialog.ErrorLaunchingSpoonCanNotLoadTransformation.Title" ),
        BaseMessages.getString( PKG, "JobGraph.Dialog.ErrorLaunchingSpoonCanNotLoadTransformation.Message" ),
        (Exception) e );
    }
  }

  public void openJob( JobEntryJob entry, JobEntryCopy jobEntryCopy ) {

    try {

      JobMeta launchJobMeta = null;

      switch ( entry.getSpecificationMethod() ) {
        case FILENAME:
          // See if this file is already loaded...
          //
          String exactFilename = jobMeta.environmentSubstitute( entry.getFilename() );
          if ( Const.isEmpty( exactFilename ) ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoFilenameSpecified" ) );
          }

          // Open the file or create a new one!
          //
          if ( KettleVFS.fileExists( exactFilename ) ) {
            launchJobMeta = new JobMeta( jobMeta, exactFilename, spoon.rep, spoon.metaStore, null );
          } else {
            launchJobMeta = new JobMeta();
          }
          launchJobMeta.setFilename( exactFilename );
          break;

        case REPOSITORY_BY_NAME:
          String exactJobname = jobMeta.environmentSubstitute( entry.getJobName() );
          String exactDirectory = jobMeta.environmentSubstitute( entry.getDirectory() );
          if ( Const.isEmpty( exactJobname ) ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoJobNameSpecified" ) );
          }
          if ( Const.isEmpty( exactDirectory ) ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoJobDirectorySpecified" ) );
          }

          // Open the job or create a new one...
          //
          RepositoryDirectoryInterface repDir = spoon.rep.findDirectory( entry.getDirectory() );
          boolean exists = spoon.rep.exists( exactJobname, repDir, RepositoryObjectType.JOB );
          if ( !exists ) {
            launchJobMeta = new JobMeta();
            launchJobMeta.setName( exactJobname );
            launchJobMeta.setRepositoryDirectory( repDir );
          } else {
            // Always reads last revision
            launchJobMeta = spoon.rep.loadJob( exactJobname, repDir, null, null );
          }
          break;
        case REPOSITORY_BY_REFERENCE:
          if ( entry.getJobObjectId() == null ) {
            throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoJobReferenceSpecified" ) );
          }
          // Always reads last revision
          launchJobMeta = spoon.rep.loadJob( entry.getJobObjectId(), null );
          break;
        default:
          break;
      }

      // If we didn't find a valid job, stop here...
      //
      if ( launchJobMeta == null ) {
        throw new Exception( BaseMessages.getString( PKG, "JobGraph.Exception.NoValidJobSpecified" ) );
      }

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

      // Try to see if this job is already loaded in another tab...
      //
      String tabName = spoon.delegates.tabs.makeTabName( launchJobMeta, true );
      TabMapEntry tabEntry = spoon.delegates.tabs.findTabMapEntry( tabName, ObjectType.JOB_GRAPH );
      if ( tabEntry != null ) {
        // Switch to this one!
        //
        spoon.tabfolder.setSelected( tabEntry.getTabItem() );
        return;
      }

      spoon.setParametersAsVariablesInUI( launchJobMeta, launchJobMeta );

      spoon.addJobGraph( launchJobMeta );
      launchJobMeta.clearChanged();

      JobGraph jobGraph = spoon.getActiveJobGraph();
      attachActiveJob( jobGraph, launchJobMeta, jobEntryCopy );

      spoon.refreshTree();
      spoon.applyVariables();

    } catch ( Throwable e ) {
      new ErrorDialog( shell,
        BaseMessages.getString( PKG, "JobGraph.Dialog.ErrorLaunchingChefCanNotLoadJob.Title" ),
        BaseMessages.getString( PKG, "JobGraph.Dialog.ErrorLaunchingChefCanNotLoadJob.Message" ), e );
    }
  }

  /**
   * Finds the last active transformation in the running job to the opened transMeta
   *
   * @param transGraph
   * @param jobEntryCopy
   */
  private void attachActiveTrans( TransGraph transGraph, TransMeta newTrans, JobEntryCopy jobEntryCopy ) {
    if ( job != null && transGraph != null ) {
      Trans trans = spoon.findActiveTrans( job, jobEntryCopy );
      transGraph.setTrans( trans );
      if ( !transGraph.isExecutionResultsPaneVisible() ) {
        transGraph.showExecutionResults();
      }
      transGraph.setControlStates();
    }
  }

  /**
   * Finds the last active job in the running job to the openened jobMeta
   *
   * @param jobGraph
   * @param newJob
   */
  private void attachActiveJob( JobGraph jobGraph, JobMeta newJobMeta, JobEntryCopy jobEntryCopy ) {
    if ( job != null && jobGraph != null ) {
      Job subJob = spoon.findActiveJob( job, jobEntryCopy );
      if ( subJob != null ) {
        jobGraph.setJob( subJob );
        jobGraph.jobGridDelegate.setJobTracker( subJob.getJobTracker() );
        if ( !jobGraph.isExecutionResultsPaneVisible() ) {
          jobGraph.showExecutionResults();
        }
        jobGraph.setControlStates();
      }
    }
  }

  public synchronized void setJob( Job job ) {
    this.job = job;
  }

  public static void copyInternalJobVariables( JobMeta sourceJobMeta, TransMeta targetTransMeta ) {
    // Also set some internal JOB variables...
    //
    String[] internalVariables = Const.INTERNAL_JOB_VARIABLES;

    for ( String variableName : internalVariables ) {
      targetTransMeta.setVariable( variableName, sourceJobMeta.getVariable( variableName ) );
    }
  }

  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 = getJobImage( disp, area.x, area.y, magnification );
    e.gc.drawImage( img, 0, 0 );
    if ( jobMeta.nrJobEntries() == 0 ) {
      e.gc.setForeground( GUIResource.getInstance().getColorCrystalTextPentaho() );
      e.gc.setBackground( GUIResource.getInstance().getColorBackground() );
      e.gc.setFont( GUIResource.getInstance().getFontMedium() );

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

    }
    img.dispose();

  }

  public Image getJobImage( 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;

    JobPainter jobPainter =
      new JobPainter(
        gc, jobMeta, new Point( x, y ), new SwtScrollBar( hori ), new SwtScrollBar( vert ), hop_candidate,
        drop_candidate, selectionRegion, areaOwners, mouseOverEntries, PropsUI.getInstance().getIconSize(),
        PropsUI.getInstance().getLineWidth(), gridSize, PropsUI
          .getInstance().getShadowSize(), PropsUI.getInstance().isAntiAliasingEnabled(), PropsUI
          .getInstance().getNoteFont().getName(), PropsUI.getInstance().getNoteFont().getHeight() );

    jobPainter.setMagnification( magnificationFactor );
    jobPainter.setEntryLogMap( entryLogMap );
    jobPainter.setStartHopEntry( startHopEntry );
    jobPainter.setEndHopLocation( endHopLocation );
    jobPainter.setEndHopEntry( endHopEntry );
    jobPainter.setNoInputEntry( noInputEntry );
    if ( job != null ) {
      jobPainter.setJobEntryResults( job.getJobEntryResults() );
    } else {
      jobPainter.setJobEntryResults( new ArrayList<JobEntryResult>() );
    }

    List<JobEntryCopy> activeJobEntries = new ArrayList<JobEntryCopy>();
    if ( job != null ) {
      if ( job.getActiveJobEntryJobs().size() > 0 ) {
        activeJobEntries.addAll( job.getActiveJobEntryJobs().keySet() );
      }
      if ( job.getActiveJobEntryTransformations().size() > 0 ) {
        activeJobEntries.addAll( job.getActiveJobEntryTransformations().keySet() );
      }
    }
    jobPainter.setActiveJobEntries( activeJobEntries );

    jobPainter.drawJob();

    return (Image) gc.getImage();
  }

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

  protected void newHop() {
    List<JobEntryCopy> selection = jobMeta.getSelectedEntries();
    if ( selection == null || selection.size() < 2 ) {
      return;
    }
    JobEntryCopy fr = selection.get( 0 );
    JobEntryCopy to = selection.get( 1 );
    spoon.newJobHop( jobMeta, fr, to );
  }

  protected void editEntry( JobEntryCopy je ) {
    spoon.editJobEntry( jobMeta, je );
  }

  protected void editNote( NotePadMeta ni ) {
    NotePadMeta before = (NotePadMeta) ni.clone();
    String title = BaseMessages.getString( PKG, "JobGraph.Dialog.EditNote.Title" );

    NotePadDialog dd = new NotePadDialog( jobMeta, 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() );

      spoon.addUndoChange( jobMeta, new NotePadMeta[] { before }, new NotePadMeta[] { ni }, new int[] { jobMeta
        .indexOfNote( ni ) } );
      ni.width = ConstUI.NOTE_MIN_SIZE;
      ni.height = ConstUI.NOTE_MIN_SIZE;
      spoon.refreshGraph();
    }
  }

  protected void drawArrow( GC gc, int[] line ) {
    int mx, my;
    int x1 = line[0] + offset.x;
    int y1 = line[1] + offset.y;
    int x2 = line[2] + offset.x;
    int y2 = line[3] + offset.y;
    int x3;
    int y3;
    int x4;
    int y4;
    int a, b, dist;
    double factor;
    double angle;

    // gc.setLineWidth(1);
    // WuLine(gc, black, x1, y1, x2, y2);

    gc.drawLine( x1, y1, x2, y2 );

    // What's the distance between the 2 points?
    a = Math.abs( x2 - x1 );
    b = Math.abs( y2 - y1 );
    dist = (int) Math.sqrt( a * a + b * b );

    // determine factor (position of arrow to left side or right side 0-->100%)
    if ( dist >= 2 * iconsize ) {
      factor = 1.5;
    } else {
      factor = 1.2;
    }

    // in between 2 points
    mx = (int) ( x1 + factor * ( x2 - x1 ) / 2 );
    my = (int) ( y1 + factor * ( y2 - y1 ) / 2 );

    // calculate points for arrowhead
    angle = Math.atan2( y2 - y1, x2 - x1 ) + Math.PI;

    x3 = (int) ( mx + Math.cos( angle - theta ) * size );
    y3 = (int) ( my + Math.sin( angle - theta ) * size );

    x4 = (int) ( mx + Math.cos( angle + theta ) * size );
    y4 = (int) ( my + Math.sin( angle + theta ) * size );

    // draw arrowhead
    // gc.drawLine(mx, my, x3, y3);
    // gc.drawLine(mx, my, x4, y4);
    // gc.drawLine( x3, y3, x4, y4 );
    Color fore = gc.getForeground();
    Color back = gc.getBackground();
    gc.setBackground( fore );
    gc.fillPolygon( new int[] { mx, my, x3, y3, x4, y4 } );
    gc.setBackground( back );
  }

  protected 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;
  }

  protected 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;
  }

  protected SnapAllignDistribute createSnapAllignDistribute() {

    List<JobEntryCopy> elements = jobMeta.getSelectedEntries();
    int[] indices = jobMeta.getEntryIndexes( elements );
    return new SnapAllignDistribute( jobMeta, elements, indices, spoon, this );
  }

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

  protected 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();
  }

  protected void drawRect( GC gc, Rectangle rect ) {
    if ( rect == null ) {
      return;
    }

    gc.setLineStyle( SWT.LINE_DASHDOT );
    gc.setLineWidth( 1 );
    gc.setForeground( GUIResource.getInstance().getColorDarkGray() );
    // PDI-2619: SWT on Windows doesn't cater for negative rect.width/height so handle here.
    Point s = new Point( rect.x + offset.x, rect.y + offset.y );
    if ( rect.width < 0 ) {
      s.x = s.x + rect.width;
    }
    if ( rect.height < 0 ) {
      s.y = s.y + rect.height;
    }
    gc.drawRoundRectangle( s.x, s.y, Math.abs( rect.width ), Math.abs( rect.height ), 3, 3 );
    gc.setLineStyle( SWT.LINE_SOLID );
  }

  protected void detach( JobEntryCopy je ) {
    JobHopMeta hfrom = jobMeta.findJobHopTo( je );
    JobHopMeta hto = jobMeta.findJobHopFrom( je );

    if ( hfrom != null && hto != null ) {
      if ( jobMeta.findJobHop( hfrom.getFromEntry(), hto.getToEntry() ) == null ) {
        JobHopMeta hnew = new JobHopMeta( hfrom.getFromEntry(), hto.getToEntry() );
        jobMeta.addJobHop( hnew );
        spoon.addUndoNew( jobMeta, new JobHopMeta[] { (JobHopMeta) hnew.clone() }, new int[] { jobMeta
          .indexOfJobHop( hnew ) } );
      }
    }
    if ( hfrom != null ) {
      int fromidx = jobMeta.indexOfJobHop( hfrom );
      if ( fromidx >= 0 ) {
        jobMeta.removeJobHop( fromidx );
        spoon.addUndoDelete( jobMeta, new JobHopMeta[] { hfrom }, new int[] { fromidx } );
      }
    }
    if ( hto != null ) {
      int toidx = jobMeta.indexOfJobHop( hto );
      if ( toidx >= 0 ) {
        jobMeta.removeJobHop( toidx );
        spoon.addUndoDelete( jobMeta, new JobHopMeta[] { hto }, new int[] { toidx } );
      }
    }
    spoon.refreshTree();
    redraw();
  }

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

  public String toString() {
    if ( jobMeta == null ) {
      return Spoon.APP_NAME;
    } else {
      return jobMeta.getName();
    }
  }

  public EngineMetaInterface getMeta() {
    return jobMeta;
  }

  /**
   * @return the jobMeta / public JobMeta getJobMeta() { return jobMeta; }
   *
   *         /**
   * @param jobMeta
   *          the jobMeta to set
   */
  public void setJobMeta( JobMeta jobMeta ) {
    this.jobMeta = jobMeta;
  }

  public boolean applyChanges() throws KettleException {
    return spoon.saveToFile( jobMeta );
  }

  public boolean canBeClosed() {
    return !jobMeta.hasChanged();
  }

  public JobMeta getManagedObject() {
    return jobMeta;
  }

  public boolean hasContentChanged() {
    return jobMeta.hasChanged();
  }

  public static int showChangedWarning( Shell shell, String name ) {
    MessageBox mb = new MessageBox( shell, SWT.YES | SWT.NO | SWT.CANCEL | SWT.ICON_WARNING );
    mb.setMessage( BaseMessages.getString( PKG, "JobGraph.Dialog.PromptSave.Message", name ) );
    mb.setText( BaseMessages.getString( PKG, "JobGraph.Dialog.PromptSave.Title" ) );
    return mb.open();
  }

  public static boolean editProperties( JobMeta jobMeta, Spoon spoon, Repository rep, boolean allowDirectoryChange ) {
    if ( jobMeta == null ) {
      return false;
    }

    JobDialog jd = new JobDialog( spoon.getShell(), SWT.NONE, jobMeta, rep );
    jd.setDirectoryChangeAllowed( allowDirectoryChange );
    JobMeta ji = jd.open();

    // In this case, load shared objects
    //
    if ( jd.isSharedObjectsFileChanged() ) {
      try {
        SharedObjects sharedObjects =
          rep != null ? rep.readJobMetaSharedObjects( jobMeta ) : jobMeta.readSharedObjects();
        spoon.sharedObjectsFileMap.put( sharedObjects.getFilename(), sharedObjects );
      } catch ( Exception e ) {
        new ErrorDialog( spoon.getShell(),
          BaseMessages.getString( PKG, "Spoon.Dialog.ErrorReadingSharedObjects.Title" ),
          BaseMessages.getString( PKG, "Spoon.Dialog.ErrorReadingSharedObjects.Message", spoon.delegates.tabs.makeTabName( jobMeta, true ) ), e );
      }
    }

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

    if ( jd.isSharedObjectsFileChanged() || ji != null ) {
      spoon.refreshTree();
      spoon.delegates.tabs.renameTabs(); // cheap operation, might as will do it anyway
    }

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

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

  /**
   * @param lastMove
   *          the lastMove to set
   */
  public void setLastMove( Point lastMove ) {
    this.lastMove = lastMove;
  }

  /**
   * 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, "JobGraph.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() {
      public void mouseDown( MouseEvent e ) {
        disposeExtraView();
      }
    } );

    minMaxButton = new Label( extraViewComposite, SWT.NONE );
    minMaxButton.setImage( GUIResource.getInstance().getImageMaximizePanel() );
    minMaxButton
      .setToolTipText( BaseMessages.getString( PKG, "JobGraph.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() {
      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, "JobLog.ResultsPanel.NameLabel" ) );
    FormData fdResultsLabel = new FormData();
    fdResultsLabel.left = new FormAttachment( 0, 0 );
    fdResultsLabel.right = new FormAttachment( 100, 0 );
    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, } );
  }

  /**
   * 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( "job-show-results" );
    button.setTooltiptext( BaseMessages.getString( PKG, "Spoon.Tooltip.ShowExecutionResults" ) );
    ToolItem swtToolItem = (ToolItem) button.getManagedObject();
    swtToolItem.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, "JobGraph.ExecutionResultsPanel.MaxButton.Tooltip" ) );
    } else {
      // Maximize
      //
      sashForm.setMaximizedControl( extraViewComposite );
      minMaxButton.setImage( GUIResource.getInstance().getImageMinimizePanel() );
      minMaxButton.setToolTipText( BaseMessages
        .getString( PKG, "JobGraph.ExecutionResultsPanel.MinButton.Tooltip" ) );
    }
  }

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

  public void showExecutionResults() {
    if ( isExecutionResultsPaneVisible() ) {
      disposeExtraView();
    } else {
      addAllTabs();
    }
  }

  public void addAllTabs() {

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

    jobHistoryDelegate.addJobHistory();
    jobLogDelegate.addJobLog();
    jobGridDelegate.addJobGrid();
    jobMetricsDelegate.addJobMetrics();

    if ( tabItemSelection != null ) {
      extraViewTabFolder.setSelection( tabItemSelection );
    } else {
      extraViewTabFolder.setSelection( jobGridDelegate.getJobGridTab() );
    }

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

  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 runJob() {
    spoon.runFile();
  }

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

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

  public XulToolbar getToolbar() {
    return toolbar;
  }

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

  public void browseVersionHistory() {
    try {
      RepositoryRevisionBrowserDialogInterface dialog =
        RepositoryExplorerDialog.getVersionBrowserDialog( shell, spoon.rep, jobMeta );
      String versionLabel = dialog.open();
      if ( versionLabel != null ) {
        spoon.loadObjectFromRepository( jobMeta.getName(), jobMeta.getRepositoryElementType(), jobMeta
          .getRepositoryDirectory(), versionLabel );
      }
    } catch ( Exception e ) {
      new ErrorDialog(
        shell, BaseMessages.getString( PKG, "JobGraph.VersionBrowserException.Title" ), BaseMessages.getString(
          PKG, "JobGraph.VersionBrowserException.Message" ), e );
    }
  }

  public synchronized void startJob( JobExecutionConfiguration executionConfiguration ) throws KettleException {

    // If the job is not running, start the transformation...
    //
    if ( job == null || job.isFinished() && !job.isActive() ) {
      // Auto save feature...
      //
      handleJobMetaChanges( jobMeta );

      // Is the repository available & name / id set?
      // Is there a filename set and no repository available?
      //
      if ( ( ( jobMeta.getName() != null && jobMeta.getObjectId() != null && spoon.rep != null ) || ( jobMeta
        .getFilename() != null && spoon.rep == null ) )
        && !jobMeta.hasChanged() // Didn't change
      ) {
        if ( job == null || ( job != null && !job.isActive() ) ) {
          try {

            // Make sure we clear the log before executing again...
            //
            if ( executionConfiguration.isClearingLog() ) {
              jobLogDelegate.clearLog();
            }

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

            JobMeta runJobMeta;

            if ( spoon.rep != null ) {
              runJobMeta = spoon.rep.loadJob( jobMeta.getName(), jobMeta.getRepositoryDirectory(), null, null );
            } else {
              runJobMeta = new JobMeta( null, jobMeta.getFilename(), null, jobMeta.getMetaStore(), null );
            }

            String spoonObjectId = UUID.randomUUID().toString();
            SimpleLoggingObject spoonLoggingObject =
              new SimpleLoggingObject( "SPOON", LoggingObjectType.SPOON, null );
            spoonLoggingObject.setContainerObjectId( spoonObjectId );
            spoonLoggingObject.setLogLevel( executionConfiguration.getLogLevel() );
            job = new Job( spoon.rep, runJobMeta, spoonLoggingObject );

            job.setLogLevel( executionConfiguration.getLogLevel() );
            job.shareVariablesWith( jobMeta );
            job.setInteractive( true );
            job.setGatheringMetrics( executionConfiguration.isGatheringMetrics() );
            job.setArguments( executionConfiguration.getArgumentStrings() );

            // Pass specific extension points...
            //
            job.getExtensionDataMap().putAll( executionConfiguration.getExtensionOptions() );

            // Add job entry listeners
            //
            job.addJobEntryListener( createRefreshJobEntryListener() );

            // If there is an alternative start job entry, pass it to the job
            //
            if ( !Const.isEmpty( executionConfiguration.getStartCopyName() ) ) {
              JobEntryCopy startJobEntryCopy =
                runJobMeta.findJobEntry( executionConfiguration.getStartCopyName(), executionConfiguration
                  .getStartCopyNr(), false );
              job.setStartJobEntryCopy( startJobEntryCopy );
            }

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

            log.logMinimal( BaseMessages.getString( PKG, "JobLog.Log.StartingJob" ) );
            job.start();
            jobGridDelegate.previousNrItems = -1;
            // Link to the new jobTracker!
            jobGridDelegate.jobTracker = job.getJobTracker();

            // Attach a listener to notify us that the transformation has
            // finished.
            job.addJobListener( new JobAdapter() {
              public void jobFinished( Job job ) {
                JobGraph.this.jobFinished();
              }
            } );

            // Show the execution results views
            //
            addAllTabs();
          } catch ( KettleException e ) {
            new ErrorDialog(
              shell, BaseMessages.getString( PKG, "JobLog.Dialog.CanNotOpenJob.Title" ), BaseMessages.getString(
                PKG, "JobLog.Dialog.CanNotOpenJob.Message" ), e );
            job = null;
          }
        } else {
          MessageBox m = new MessageBox( shell, SWT.OK | SWT.ICON_WARNING );
          m.setText( BaseMessages.getString( PKG, "JobLog.Dialog.JobIsAlreadyRunning.Title" ) );
          m.setMessage( BaseMessages.getString( PKG, "JobLog.Dialog.JobIsAlreadyRunning.Message" ) );
          m.open();
        }
      } else {
        if ( jobMeta.hasChanged() ) {
          MessageBox m = new MessageBox( shell, SWT.OK | SWT.ICON_WARNING );
          m.setText( BaseMessages.getString( PKG, "JobLog.Dialog.JobHasChangedSave.Title" ) );
          m.setMessage( BaseMessages.getString( PKG, "JobLog.Dialog.JobHasChangedSave.Message" ) );
          m.open();
        } else if ( spoon.rep != null && jobMeta.getName() == null ) {
          MessageBox m = new MessageBox( shell, SWT.OK | SWT.ICON_WARNING );
          m.setText( BaseMessages.getString( PKG, "JobLog.Dialog.PleaseGiveThisJobAName.Title" ) );
          m.setMessage( BaseMessages.getString( PKG, "JobLog.Dialog.PleaseGiveThisJobAName.Message" ) );
          m.open();
        } else {
          MessageBox m = new MessageBox( shell, SWT.OK | SWT.ICON_WARNING );
          m.setText( BaseMessages.getString( PKG, "JobLog.Dialog.NoFilenameSaveYourJobFirst.Title" ) );
          m.setMessage( BaseMessages.getString( PKG, "JobLog.Dialog.NoFilenameSaveYourJobFirst.Message" ) );
          m.open();
        }
      }
      setControlStates();
    }
  }

  private JobEntryListener createRefreshJobEntryListener() {
    return new JobEntryListener() {

      public void beforeExecution( Job job, JobEntryCopy jobEntryCopy, JobEntryInterface jobEntryInterface ) {
        asyncRedraw();
      }

      public void afterExecution( Job job, JobEntryCopy jobEntryCopy, JobEntryInterface jobEntryInterface,
        Result result ) {
        asyncRedraw();
      }
    };
  }

  /**
   * This gets called at the very end, when everything is done.
   */
  protected void jobFinished() {
    // Do a final check to see if it all ended...
    //
    if ( job != null && job.isInitialized() && job.isFinished() ) {
      for ( RefreshListener listener : refreshListeners ) {
        listener.refreshNeeded();
      }
      jobMetricsDelegate.resetLastRefreshTime();
      jobMetricsDelegate.updateGraph();
      log.logMinimal( BaseMessages.getString( PKG, "JobLog.Log.JobHasEnded" ) );
    }
    setControlStates();
  }

  public synchronized void stopJob() {
    if ( job != null && job.isActive() && job.isInitialized() ) {
      job.stopAll();
      job.waitUntilFinished( 5000 ); // wait until everything is stopped, maximum 5 seconds...

      log.logMinimal( BaseMessages.getString( PKG, "JobLog.Log.JobWasStopped" ) );
    }
    setControlStates();
  }

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

  public void setControlStates() {
    if ( isDisposed() || getDisplay().isDisposed() ) {
      return;
    }

    getDisplay().asyncExec( new Runnable() {

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

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

        // Stop button...
        //
        XulToolbarbutton stopButton = (XulToolbarbutton) toolbar.getElementById( "job-stop" );
        if ( stopButton != null && !controlDisposed( stopButton ) ) {
          if ( stopButton.isDisabled() ^ !running ) {
            stopButton.setDisabled( !running );
          }
        }

        // Replay button...
        //
        XulToolbarbutton replayButton = (XulToolbarbutton) toolbar.getElementById( "job-replay" );
        if ( replayButton != null && !controlDisposed( replayButton ) && !operationsNotAllowed ) {
          if ( replayButton.isDisabled() ^ running ) {
            replayButton.setDisabled( running );
          }
        }

        // version browser button...
        //
        XulToolbarbutton versionsButton = (XulToolbarbutton) toolbar.getElementById( "browse-versions" );
        if ( versionsButton != null && !controlDisposed( versionsButton ) ) {
          boolean hasRepository = spoon.rep != null;
          boolean enabled =
            hasRepository && spoon.rep.getRepositoryMeta().getRepositoryCapabilities().supportsRevisions();
          if ( versionsButton.isDisabled() ^ !enabled ) {
            versionsButton.setDisabled( !enabled );
          }
        }
      }

    } );

  }

  /**
   * @return the refresh listeners
   */
  public List<RefreshListener> getRefreshListeners() {
    return refreshListeners;
  }

  /**
   * @param refreshListeners
   *          the refresh listeners to set
   */
  public void setRefreshListeners( List<RefreshListener> refreshListeners ) {
    this.refreshListeners = refreshListeners;
  }

  /**
   * @param refreshListener
   *          the job refresh listener to add
   */
  public void addRefreshListener( RefreshListener refreshListener ) {
    refreshListeners.add( refreshListener );
  }

  public String getName() {
    return "jobgraph";
  }

  public XulDomContainer getXulDomContainer() {
    return xulDomContainer;
  }

  /*
   * (non-Javadoc)
   *
   * @see org.pentaho.ui.xul.impl.XulEventHandler#setName(java.lang.String)
   */
  public void setName( String name ) {
    // TODO Auto-generated method stub

  }

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

  public boolean canHandleSave() {
    return true;
  }

  public HasLogChannelInterface getLogChannelProvider() {
    return job;
  }

  // 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!
    jobMeta.addUndo( obj, null, pos, prev, curr, TransMeta.TYPE_UNDO_POSITION, nextAlso );
    spoon.setUndoMenu( jobMeta );
  }

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

  public void replayJob() {
    List<JobEntryCopy> selectedEntries = jobMeta.getSelectedEntries();
    if ( selectedEntries.size() != 1 ) {
      MessageBox box = new MessageBox( shell, SWT.ICON_INFORMATION | SWT.CLOSE );
      box.setText( BaseMessages.getString( PKG, "JobGraph.ReplayJob.SelectOneEntryToStartFrom.Title" ) );
      box.setMessage( BaseMessages.getString( PKG, "JobGraph.ReplayJob.SelectOneEntryToStartFrom.Message" ) );
      box.open();
      return;
    }

    JobEntryCopy copy = selectedEntries.get( 0 );

    spoon.executeJob( jobMeta, true, false, null, false, copy.getName(), copy.getNr() );
  }

  public void handleJobMetaChanges( JobMeta jobMeta ) throws KettleException {
    if ( jobMeta.hasChanged() ) {
      if ( spoon.props.getAutoSave() ) {
        if ( log.isDetailed() ) {
          log.logDetailed( BaseMessages.getString( PKG, "JobLog.Log.AutoSaveFileBeforeRunning" ) );
        }
        spoon.saveToFile( jobMeta );
      } else {
        MessageDialogWithToggle md =
          new MessageDialogWithToggle(
            shell, BaseMessages.getString( PKG, "JobLog.Dialog.SaveChangedFile.Title" ), null, BaseMessages
              .getString( PKG, "JobLog.Dialog.SaveChangedFile.Message" )
              + Const.CR
              + BaseMessages.getString( PKG, "JobLog.Dialog.SaveChangedFile.Message2" )
              + Const.CR,
            MessageDialog.QUESTION,
            new String[] {
              BaseMessages.getString( PKG, "System.Button.Yes" ),
              BaseMessages.getString( PKG, "System.Button.No" ) },
            0, BaseMessages.getString( PKG, "JobLog.Dialog.SaveChangedFile.Toggle" ), spoon.props.getAutoSave() );
        int answer = md.open();
        if ( ( answer & 0xFF ) == 0 ) {
          spoon.saveToFile( jobMeta );
        }
        spoon.props.setAutoSave( md.getToggleState() );
      }
    }
  }

  private JobEntryCopy lastChained = null;

  public void addJobEntryToChain( String typeDesc, boolean shift ) {
    JobMeta jobMeta = spoon.getActiveJob();
    if ( jobMeta == null ) {
      return;
    }

    //Is the lastChained entry still valid?
    //
    if ( lastChained != null && jobMeta.findJobEntry( lastChained.getName(), lastChained.getNr(), false ) == null ) {
      lastChained = null;
    }

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

    // Where do we add this?

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

    p.x += 200;

    // Which is the new entry?

    JobEntryCopy newEntry = spoon.newJobEntry( jobMeta, typeDesc, false );
    if ( newEntry == null ) {
      return;
    }
    newEntry.setLocation( p.x, p.y );
    newEntry.setDrawn();

    if ( lastChained != null ) {
      spoon.newJobHop( jobMeta, lastChained, newEntry );
    }

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

    if ( shift ) {
      editEntry( newEntry );
    }

    jobMeta.unselectAll();
    newEntry.setSelected( true );
  }

  public Spoon getSpoon() {
    return spoon;
  }

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

  public JobMeta getJobMeta() {
    return jobMeta;
  }

  public Job getJob() {
    return job;
  }
}
