org.pentaho.di.ui.spoon.job.JobGraph.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.di.ui.spoon.job.JobGraph.java

Source

// CHECKSTYLE:FileLength:OFF
/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2019 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.pentaho.di.ui.spoon.job;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
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.util.Utils;
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.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<>();

    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.clearSearchFilter();

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

        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 && areaOwner.getAreaType() != 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());
                    if (hop.isEnabled() && (jobMeta.hasLoop(hop.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) {
                            hop.setEnabled(false);
                        }
                    }
                    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 && areaOwner.getAreaType() != 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<>();
            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<>();
            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;
        boolean isDeprecated = false;

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

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

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

    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.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 && areaOwner.getAreaType() != 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 super.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(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(Utils.isEmpty(referencedObjects));

                launchMenu.removeChildren();

                if (!Utils.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()) {
                            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");
        NotePadDialog dd = new NotePadDialog(jobMeta, shell, title);
        NotePadMeta n = dd.open();
        if (n != null) {
            NotePadMeta npi = new NotePadMeta(n.getNote(), lastclick.x, lastclick.y, ConstUI.NOTE_MIN_SIZE,
                    ConstUI.NOTE_MIN_SIZE, n.getFontName(), n.getFontSize(), n.isFontBold(), n.isFontItalic(),
                    n.getFontColorRed(), n.getFontColorGreen(), n.getFontColorBlue(), n.getBackGroundColorRed(),
                    n.getBackGroundColorGreen(), n.getBackGroundColorBlue(), n.getBorderColorRed(),
                    n.getBorderColorGreen(), n.getBorderColorBlue(), n.isDrawShadow());
            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.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.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.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) });

        Set<JobEntryCopy> checkedEntries = enableDisableNextHops(currentHop.getToEntry(), enabled, new HashSet<>());

        if (checkedEntries.stream().anyMatch(entry -> jobMeta.hasLoop(entry))) {
            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();
    }

    private Set<JobEntryCopy> enableDisableNextHops(JobEntryCopy from, boolean enabled,
            Set<JobEntryCopy> checkedEntries) {
        checkedEntries.add(from);
        jobMeta.getJobhops().stream().filter(hop -> from.equals(hop.getFromEntry())).forEach(hop -> {
            if (hop.isEnabled() != enabled) {
                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 (!checkedEntries.contains(hop.getToEntry())) {
                enableDisableNextHops(hop.getToEntry(), enabled, checkedEntries);
            }
        });
        return checkedEntries;
    }

    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 && areaOwner.getAreaType() != 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<>(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;
            case JOB_ENTRY_ICON:
                JobEntryCopy jec = (JobEntryCopy) areaOwner.getOwner();
                if (jec.isDeprecated()) { // only need tooltip if job entry is deprecated
                    tip.append(BaseMessages.getString(PKG, "JobGraph.DeprecatedEntry.Tooltip.Title"))
                            .append(Const.CR);
                    String tipNext = BaseMessages.getString(PKG, "JobGraph.DeprecatedEntry.Tooltip.Message1",
                            jec.getName());
                    int length = tipNext.length() + 5;
                    for (int i = 0; i < length; i++) {
                        tip.append("-");
                    }
                    tip.append(Const.CR).append(tipNext).append(Const.CR);
                    tip.append(BaseMessages.getString(PKG, "JobGraph.DeprecatedEntry.Tooltip.Message2"));
                    if (!Utils.isEmpty(jec.getSuggestion())
                            && !(jec.getSuggestion().startsWith("!") && jec.getSuggestion().endsWith("!"))) {
                        tip.append(" ");
                        tip.append(BaseMessages.getString(PKG, "JobGraph.DeprecatedEntry.Tooltip.Message3",
                                jec.getSuggestion()));
                    }
                    tipImage = GUIResource.getInstance().getImageDeprecated();
                }
                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 (!Utils.isEmpty(references)) {
            loadReferencedObject(jobEntryCopy, 0);
        }

        /*
         * if (jobEntryCopy.isJob()) { final JobEntryJob entry = (JobEntryJob) jobEntryCopy.getEntry(); if ((entry != null
         * && entry.getJobObjectId() == null && !Utils.isEmpty(entry.getFilename()) && spoon.rep == null) || (entry != null
         * && !Utils.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 && !Utils.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 (Utils.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 (Utils.isEmpty(exactTransname)) {
                    throw new Exception(BaseMessages.getString(PKG, "JobGraph.Exception.NoTransNameSpecified"));
                }
                if (Utils.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 (Utils.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 (Utils.isEmpty(exactJobname)) {
                    throw new Exception(BaseMessages.getString(PKG, "JobGraph.Exception.NoJobNameSpecified"));
                }
                if (Utils.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<>();
        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.isStopped()) && !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 (!Utils.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()) {
                    showSaveFileMessage();
                } 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();
        }
    }

    public void showSaveFileMessage() {
        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();
    }

    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());
                    spoon.handleRepositoryLost(krle);
                }

                // 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 new HasLogChannelInterface() {
            @Override
            public LogChannelInterface getLogChannel() {
                return getJob() != null ? getJob().getLogChannel() : getJobMeta().getLogChannel();
            }
        };
    }

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