diff --git a/src/main/java/plugins/adufour/blocks/lang/ArrayLoop.java b/src/main/java/plugins/adufour/blocks/lang/ArrayLoop.java
new file mode 100644
index 0000000000000000000000000000000000000000..e11ef3e52c3025b7b8c225a6c64e342c3242d12e
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/ArrayLoop.java
@@ -0,0 +1,9 @@
+package plugins.adufour.blocks.lang;
+
+/**
+ * @deprecated use {@link Batch} instead.
+ * @author Alexandre Dufour
+ */
+@Deprecated
+public class ArrayLoop extends Batch
+{}
\ No newline at end of file
diff --git a/src/main/java/plugins/adufour/blocks/lang/Batch.java b/src/main/java/plugins/adufour/blocks/lang/Batch.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa8ac3f30dd2f1f9af93104a099eaddc1836e7a3
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/Batch.java
@@ -0,0 +1,155 @@
+package plugins.adufour.blocks.lang;
+
+import java.lang.reflect.Array;
+import java.util.List;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarArray;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarMutableArray;
+import plugins.adufour.vars.util.VarException;
+import plugins.adufour.vars.util.VarListener;
+
+/**
+ * A batch is a particular type of work-flow that will execute repeatedly for every element of a
+ * list of items, allowing to process all list items with a same work-flow. This is the most generic
+ * type of batch available, however there are more user-friendly versions available, such as for
+ * instance a {@link FileBatch File batch} or a {@link SequenceFileBatch Sequence file batch}.
+ * 
+ * @author Alexandre Dufour
+ */
+public class Batch extends Loop
+{
+    private VarMutableArray array;
+
+    private VarMutable element;
+
+    /**
+     * Defines the variable used to determine the list of objects to batch process.<br>
+     * By default, this variable is simply a {@link VarArray} containing the elements to process,
+     * but overriding classes may provide special functionalities.
+     * 
+     * @see FileBatch#getBatchSource()
+     * @see SequenceFileBatch#getBatchSource()
+     * @see SequenceSeriesBatch#getBatchSource()
+     * @return the variable used to define the contents of the batch
+     */
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public Var<?> getBatchSource()
+    {
+        if (array == null)
+        {
+            array = new VarMutableArray("array", null);
+            array.addListener(new VarListener()
+            {
+                @Override
+                public void valueChanged(Var source, Object oldValue, Object newValue)
+                {
+                }
+
+                @Override
+                public void referenceChanged(Var source, Var oldReference, Var newReference)
+                {
+                    if (oldReference == null)
+                    {
+                        if (newReference != null)
+                        {
+                            element.setType(newReference.getType().getComponentType());
+                        }
+                    }
+                    else if (element.isReferenced())
+                    {
+                        if (newReference != null && newReference.getType() != oldReference.getType())
+                            throw new IllegalAccessError("Cannot change the type of a linked mutable variable");
+                    }
+                    else
+                    {
+                        if (newReference == null)
+                        {
+                            element.setType(null);
+                            array.setType(null);
+                        }
+                        else
+                        {
+                            element.setType(newReference.getType().getComponentType());
+                        }
+                    }
+                }
+            });
+        }
+        return array;
+    }
+
+    /**
+     * Defines the variable that will contain each element of the batch process, consecutively.
+     * 
+     * @see FileBatch#getBatchElement()
+     * @see SequenceFileBatch#getBatchElement()
+     * @see SequenceSeriesBatch#getBatchElement()
+     * @return the inner variable that will contain each element to process
+     */
+    public Var<?> getBatchElement()
+    {
+        if (element == null)
+        {
+            // WARNING: do *not* change the name of this variable
+            // why? see declareInput() below and VarList.add()
+            element = new VarMutable("element", null);
+        }
+
+        return element;
+    }
+
+    @Override
+    public void initializeLoop()
+    {
+        if (array == null || Array.getLength(array.getValue(true)) == 0)
+            throw new VarException(array, "Cannot loop on an empty array");
+    }
+
+    @Override
+    public void beforeIteration()
+    {
+        element.setValue(array.getElementAt(getIterationCounter().getValue()));
+    }
+
+    @Override
+    public boolean isStopConditionReached()
+    {
+        return getIterationCounter().getValue() == Array.getLength(array.getValue());
+    }
+
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        super.declareInput(inputMap);
+
+        Var<?> batchSource = getBatchSource();
+
+        inputMap.add(batchSource.getName(), getBatchSource());
+    }
+
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        super.declareOutput(outputMap);
+
+        Var<?> batchElement = getBatchElement();
+
+        outputMap.add(batchElement.getName(), batchElement);
+    }
+
+    /**
+     * {@inheritDoc} <br>
+     * <br>
+     * The implementation provided in this class automatically declares the source and element of
+     * the batch as loop variables, therefore they don't need to be declared by overriding classes
+     */
+    @Override
+    public void declareLoopVariables(List<Var<?>> loopVariables)
+    {
+        loopVariables.add(getBatchSource());
+        loopVariables.add(getBatchElement());
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/Block.java b/src/main/java/plugins/adufour/blocks/lang/Block.java
new file mode 100644
index 0000000000000000000000000000000000000000..55a27de2aa78b76b5fd8b4718bbcd556f09e45e3
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/Block.java
@@ -0,0 +1,33 @@
+package plugins.adufour.blocks.lang;
+
+import plugins.adufour.blocks.util.VarList;
+
+/**
+ * Interface indicating that implementing classes can be used in a block programming context
+ * 
+ * @see WorkFlow
+ * @author Alexandre Dufour
+ */
+public interface Block extends Runnable
+{
+	/**
+	 * Fills the specified map with all the necessary input variables
+	 * 
+	 * @param inputMap
+	 *            the list of input variables to fill
+	 */
+	void declareInput(final VarList inputMap);
+
+	/**
+	 * Fills the specified map with all the necessary output variables
+	 * 
+	 * @param outputMap
+	 *            the list of output variables to fill
+	 */
+	void declareOutput(final VarList outputMap);
+
+	/**
+	 * Main method
+	 */
+	void run();
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/BlockDescriptor.java b/src/main/java/plugins/adufour/blocks/lang/BlockDescriptor.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1b284fec9c2a8b92f77aff635563503cba7f6e0
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/BlockDescriptor.java
@@ -0,0 +1,1121 @@
+package plugins.adufour.blocks.lang;
+
+import java.awt.Dimension;
+import java.awt.Point;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import icy.file.xml.XMLPersistent;
+import icy.gui.plugin.PluginErrorReport;
+import icy.network.NetworkUtil;
+import icy.plugin.PluginDescriptor;
+import icy.plugin.PluginInstaller;
+import icy.plugin.PluginLoader;
+import icy.plugin.PluginRepositoryLoader;
+import icy.plugin.abstract_.Plugin;
+import icy.util.ClassUtil;
+import icy.util.StringUtil;
+import icy.util.XMLUtil;
+import plugins.adufour.blocks.tools.Display;
+import plugins.adufour.blocks.tools.input.InputBlock;
+import plugins.adufour.blocks.util.*;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarMutableArray;
+import plugins.adufour.vars.util.MutableType;
+import plugins.adufour.vars.util.VarListener;
+
+/**
+ * Class defining all the metadata associated to a {@link Block}.
+ * 
+ * @author Alexandre Dufour
+ */
+@SuppressWarnings("rawtypes")
+public class BlockDescriptor implements Runnable, VarListener, VarListListener, XMLPersistent
+{
+    public static enum BlockStatus
+    {
+        /**
+         * The result is not up-to-date. This status is active at startup and whenever a variable is
+         * changed
+         */
+        DIRTY("This block is ready to run"),
+        /**
+         * The block is currently running
+         */
+        RUNNING("This block is currently running..."),
+        /**
+         * The result is up-to-date and need not be recomputed (unless explicitly required)
+         */
+        READY("This block is up to date"),
+        /**
+         * There was an error while running this block
+         */
+        ERROR("This block did not run properly");
+
+        public final String defaultErrorMessage;
+
+        private String optionalUserMessage = "";
+
+        private BlockStatus(String message)
+        {
+            defaultErrorMessage = message;
+        }
+
+        public String getUserMessage()
+        {
+            return optionalUserMessage;
+        }
+
+        public void setUserMessage(String message)
+        {
+            optionalUserMessage = (message != null ? message : "");
+        }
+
+        @Override
+        public String toString()
+        {
+            return defaultErrorMessage + (optionalUserMessage.isEmpty() ? "" : ":\n" + optionalUserMessage);
+        }
+    }
+
+    private Integer id = hashCode();
+
+    private Block block;
+
+    private WorkFlow container;
+
+    private final HashSet<BlockListener> listeners = new HashSet<BlockListener>();
+
+    /**
+     * The input variables of this descriptor's block
+     */
+    public final VarList inputVars;
+
+    /**
+     * The output variables of this descriptor's block
+     */
+    public final VarList outputVars;
+
+    private final Point location = new Point();
+
+    private final Dimension dimension = new Dimension(0, 0);
+
+    private boolean collapsed = false;
+
+    private BlockStatus status = BlockStatus.DIRTY;
+
+    private boolean finalBlock;
+
+    private String definedName = null;
+
+    private boolean keepResults = true;
+
+    /** Command-line ID (used only for input blocks) */
+    private String commandLineID = "";
+
+    public BlockDescriptor()
+    {
+        this.inputVars = new VarList();
+        this.inputVars.addVarListListener(this);
+        this.outputVars = new VarList();
+        this.outputVars.addVarListListener(this);
+    }
+
+    public BlockDescriptor(int ID, Block block)
+    {
+        this();
+
+        this.block = block;
+
+        try
+        {
+            block.declareInput(inputVars);
+
+            block.declareOutput(outputVars);
+        }
+        catch (RuntimeException e)
+        {
+            String blockName = block.getClass().getName();
+
+            String devId = blockName.substring("plugins.".length());
+            devId = devId.substring(0, devId.indexOf('.'));
+
+            StringWriter sw = new StringWriter();
+            PrintWriter pw = new PrintWriter(sw);
+            pw.write("Unable to insert block: " + block.getClass().getName() + "\n");
+            pw.write("Reason: " + e.getClass().getName() + ": " + e.getMessage() + "\n");
+            pw.write("Stack trace:\n");
+            e.printStackTrace(pw);
+
+            PluginErrorReport.report(null, devId, sw.toString());
+        }
+
+        if (ID != -1)
+            id = ID;
+    }
+
+    /**
+     * @deprecated use {@link #BlockDescriptor(int, Block)} instead
+     * @param ID
+     *        Identifier
+     * @param block
+     *        Block template instance.
+     * @param owner
+     *        Workflow containing the new block.
+     * @param location
+     *        Position of the new block.
+     */
+    @Deprecated
+    public BlockDescriptor(int ID, Block block, WorkFlow owner, Point location)
+    {
+        this(ID, block);
+
+        this.container = owner;
+        setLocation(location.x, location.y);
+    }
+
+    /**
+     * Adds a new input variable to this block <u>after</u> it has been initialized. This method is
+     * called when the block is embedded inside another work flow, such that the block variables can
+     * be later exposed (and linked) outside the work flow.
+     * <p>
+     * NB: default (startup) block variables are not added here, they are automatically created in
+     * {@link #BlockDescriptor(int, Block)} via {@link Block#declareInput(VarList)} and
+     * {@link Block#declareOutput(VarList)}.
+     * 
+     * @param uid
+     *        a unique identifier for the variable
+     * @param variable
+     *        the variable to add
+     * @throws IllegalArgumentException
+     *         if a variable with same unique ID exists
+     */
+    public void addInput(String uid, Var<?> variable) throws IllegalArgumentException
+    {
+        inputVars.add(uid, variable);
+    }
+
+    /**
+     * Adds a new input variable to this block <u>after</u> it has been initialized. This method is
+     * called when the block is embedded inside another work flow, such that the block variables can
+     * be later exposed (and linked) outside the work flow.
+     * <p>
+     * NB: default (startup) block variables are not added here, they are automatically created in
+     * {@link #BlockDescriptor(int, Block)} via {@link Block#declareInput(VarList)} and
+     * {@link Block#declareOutput(VarList)}.
+     * 
+     * @param uid
+     *        a unique identifier for the variable
+     * @param variable
+     *        the variable to add
+     * @throws IllegalArgumentException
+     *         if a variable with same unique ID exists
+     */
+    public void addOutput(String uid, Var<?> variable) throws IllegalArgumentException
+    {
+        outputVars.add(uid, variable);
+    }
+
+    public void addBlockListener(BlockListener listener)
+    {
+        listeners.add(listener);
+    }
+
+    public void addBlockPanelListener(BlockListener listener)
+    {
+        listeners.add(listener);
+    }
+
+    public void removeBlockListener(BlockListener listener)
+    {
+        listeners.remove(listener);
+    }
+
+    public void removeBlockPanelListener(BlockListener listener)
+    {
+        listeners.remove(listener);
+    }
+
+    public Block getBlock()
+    {
+        return block;
+    }
+
+    public WorkFlow getContainer()
+    {
+        return container;
+    }
+
+    public Dimension getDimension()
+    {
+        return dimension;
+    }
+
+    public Integer getID()
+    {
+        return id;
+    }
+
+    /**
+     * @return the location of the block in the work flow (use only in graphical mode)
+     */
+    public Point getLocation()
+    {
+        return location;
+    }
+
+    /**
+     * @return The name of the block
+     */
+    public String getName()
+    {
+        String blockName = block.getClass().getSimpleName();
+
+        if (block instanceof Plugin && ((Plugin) block).getDescriptor() != null)
+        {
+            String pluginName = ((Plugin) block).getDescriptor().getName();
+
+            if (!pluginName.equalsIgnoreCase(blockName))
+                return pluginName;
+        }
+
+        if (blockName.endsWith("block"))
+        {
+            blockName = blockName.substring(0, blockName.lastIndexOf("block"));
+        }
+        else if (blockName.endsWith("Block"))
+        {
+            blockName = blockName.substring(0, blockName.lastIndexOf("Block"));
+        }
+
+        return BlocksFinder.getFlattened(blockName);
+    }
+
+    public BlockStatus getStatus()
+    {
+        return status;
+    }
+
+    /**
+     * Retrieves the unique ID of this variable. This method first searches in the list of input
+     * variables, and then in the list of output variables if necessary.
+     * 
+     * @param variable
+     *        The variable to retrieve.
+     * @return Identifier of the given variable.
+     * @throws NoSuchVariableException
+     *         If the variable cannot be found in this block's input or output.
+     */
+    public String getVarID(Var<?> variable) throws NoSuchVariableException
+    {
+        if (inputVars.contains(variable))
+        {
+            String varID = inputVars.getID(variable);
+
+            if (!isWorkFlow() || varID.contains(":"))
+                return varID;
+
+            return ((WorkFlow) block).getInputVarID(variable);
+        }
+
+        String varID = outputVars.getID(variable);
+
+        if (!isWorkFlow() || varID.contains(":"))
+            return varID;
+
+        return ((WorkFlow) block).getOutputVarID(variable);
+    }
+
+    public boolean isCollapsed()
+    {
+        return collapsed;
+    }
+
+    /**
+     * @return true if this block is the last block to execute in the work flow. If true, then the
+     *         execution of the top-level work flow will stop after execution of this block.
+     */
+    public boolean isFinalBlock()
+    {
+        return finalBlock;
+    }
+
+    public boolean isLoop()
+    {
+        return block instanceof Loop;
+    }
+
+    public boolean isSingleBlock()
+    {
+        return block instanceof Display || block instanceof InputBlock;
+    }
+
+    public boolean isWorkFlow()
+    {
+        return block instanceof WorkFlow;
+    }
+
+    public boolean isTopLevelWorkFlow()
+    {
+        // Stephane FIX: properly detect top level workflow
+        return isWorkFlow() && ((WorkFlow) block).isTopLevel();
+        // return isWorkFlow() && container == null;
+    }
+
+    public void setCollapsed(boolean collapsed)
+    {
+        if (this.collapsed == collapsed)
+            return;
+
+        this.collapsed = collapsed;
+
+        for (BlockListener l : listeners)
+            l.blockCollapsed(this, collapsed);
+    }
+
+    public void setContainer(WorkFlow container)
+    {
+        this.container = container;
+    }
+
+    /**
+     * Sets whether this block is the last block to execute in the work flow. If true, then the
+     * execution of the top-level work flow will stop after execution of this block. Note that if
+     * multiple blocks are marked as final, the execution will stop after the first block marked
+     * final, in order of execution.
+     * 
+     * @param finalBlock
+     *        true to stop the work flow after running this block, false otherwise.
+     */
+    public void setFinalBlock(boolean finalBlock)
+    {
+        this.finalBlock = finalBlock;
+    }
+
+    /**
+     * Sets the dimension of this block.
+     * 
+     * @param width
+     *        Width of the block in pixels.
+     * @param height
+     *        Height of the block in pixels.
+     */
+    public void setDimension(int width, int height)
+    {
+        if (dimension.width == width && dimension.height == height)
+            return;
+
+        dimension.setSize(width, height);
+
+        for (BlockListener l : listeners)
+            l.blockDimensionChanged(this, width, height);
+    }
+
+    /**
+     * Sets the ID of this block.
+     * 
+     * @param id
+     *        Block unique identifier.
+     */
+    public void setID(int id)
+    {
+        this.id = id;
+    }
+
+    /**
+     * Sets the value of the specified input variable
+     * 
+     * @param <T>
+     *        Type of the value of the variable being set.
+     * @param varID
+     *        The unique ID of the input variable to set.
+     * @param value
+     *        The new variable value.
+     * @throws NoSuchVariableException
+     *         If the variable is not in this block.
+     */
+    public <T> void setInput(String varID, T value) throws NoSuchVariableException
+    {
+        Var<T> input = inputVars.get(varID);
+        if (input == null)
+            throw new NoSuchVariableException(this, varID);
+
+        input.setValue(value);
+    }
+
+    /**
+     * Stores the location of the block and notifies listeners.
+     * 
+     * @param x
+     *        X-axis position in pixels.
+     * @param y
+     *        Y-axis position in pixels.
+     */
+    public void setLocation(int x, int y)
+    {
+        if (location.x == x && location.y == y)
+            return;
+
+        location.move(x, y);
+        for (BlockListener l : listeners)
+            l.blockLocationChanged(this, x, y);
+    }
+
+    /**
+     * Sets the value of the specified output variable
+     * 
+     * @param <T>
+     *        Type of the value in the variable being set.
+     * @param varID
+     *        the unique ID of the output variable to set
+     * @param value
+     *        the new variable value
+     * @throws NoSuchVariableException
+     *         if the variable is not in this block
+     */
+    public <T> void setOutput(String varID, T value) throws NoSuchVariableException
+    {
+        Var<T> output = outputVars.get(varID);
+        if (output == null)
+            throw new NoSuchVariableException(this, varID);
+
+        output.setValue(value);
+    }
+
+    public void setStatus(BlockStatus newStatus)
+    {
+        if (this.status == newStatus)
+            return;
+
+        this.status = newStatus;
+        for (BlockListener listener : listeners)
+            listener.blockStatusChanged(this, status);
+
+        if (status == BlockStatus.DIRTY && container != null)
+        {
+            // propagate dirty status to the container (if it is not running)
+            BlockDescriptor wfDescriptor = container.getBlockDescriptor();
+
+            if (wfDescriptor.getStatus() != BlockStatus.RUNNING)
+            {
+                wfDescriptor.setStatus(BlockStatus.DIRTY);
+            }
+        }
+    }
+
+    /**
+     * Removes the specified variable from the list of inputs
+     * 
+     * @param inputVar
+     *        Variable to be removed from the input variable list.
+     */
+    public void removeInput(Var<?> inputVar)
+    {
+        if (!inputVars.contains(inputVar))
+            return;
+
+        inputVars.setVisible(inputVar, false);
+        inputVars.remove(inputVar);
+    }
+
+    /**
+     * Removes the specified variable from the list of outputs
+     * 
+     * @param outputVar
+     *        Variable to be removed from the output variable list.
+     */
+    public void removeOutput(Var<?> outputVar)
+    {
+        if (!outputVars.contains(outputVar))
+            return;
+
+        outputVars.setVisible(outputVar, false);
+        outputVars.remove(outputVar);
+    }
+
+    /**
+     * Resets all output variables in this block to their default value
+     */
+    @SuppressWarnings("unchecked")
+    public void reset()
+    {
+        setStatus(BlockStatus.DIRTY);
+
+        for (Var var : outputVars)
+            var.setValue(var.getDefaultValue());
+
+        if (isWorkFlow())
+            ((WorkFlow) block).reset();
+    }
+
+    public void run()
+    {
+        if (keepResults && status == BlockStatus.READY)
+            return;
+
+        setStatus(BlockStatus.RUNNING);
+
+        try
+        {
+            block.run();
+            setStatus(keepResults ? BlockStatus.READY : BlockStatus.DIRTY);
+        }
+        catch (RuntimeException e)
+        {
+            BlockStatus error = BlockStatus.ERROR;
+
+            error.setUserMessage(e.getMessage());
+
+            setStatus(error);
+            throw e;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void valueChanged(Var source, Object oldValue, Object newValue)
+    {
+        if (inputVars.contains(source))
+            setStatus(BlockStatus.DIRTY);
+
+        for (BlockListener listener : listeners)
+            listener.blockVariableChanged(this, source, newValue);
+    }
+
+    @Override
+    public void referenceChanged(Var source, Var oldReference, Var newReference)
+    {
+        setStatus(BlockStatus.DIRTY);
+    }
+
+    @Override
+    public String toString()
+    {
+        return getContainer().getBlockDescriptor().getName() + "." + getName();
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void variableAdded(VarList list, Var<?> variable)
+    {
+        variable.addListener(this);
+
+        for (BlockListener listener : listeners)
+            listener.blockVariableAdded(this, variable);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void variableRemoved(VarList list, Var<?> variable)
+    {
+        variable.removeListener(this);
+
+        // although we removed a variable, there is no "removed" notifier
+        // use the "added" notifier just for the sake of refreshing the GUI
+        for (BlockListener listener : listeners)
+            listener.blockVariableAdded(this, variable);
+    }
+
+    /**
+     * Goes online, downloads and installs the plug-in containing the specified block
+     * 
+     * @param blockType
+     * @return
+     * @throws ClassNotFoundException
+     */
+    @SuppressWarnings("unchecked")
+    private Class<? extends Block> installRequiredBlock(String pluginClassName, String blockType)
+            throws ClassNotFoundException
+    {
+        Class<?> clazz = null;
+
+        // check if the class exists, otherwise try downloading it (if online)
+        try
+        {
+            clazz = ClassUtil.findClass(blockType);
+        }
+        catch (ClassNotFoundException e)
+        {
+            if (!NetworkUtil.hasInternetAccess() || getClass().getClassLoader() == ClassLoader.getSystemClassLoader())
+            {
+                throw new BlocksException("Plugin " + blockType
+                        + " is missing, but no internet connection is available.\nTry again later", true);
+            }
+
+            // String simpleName =
+            // ClassUtil.getSimpleClassName(ClassUtil.getBaseClassName(blockType));
+
+            // status.setValue("Downloading " + simpleName + "...");
+
+            PluginDescriptor pd = PluginRepositoryLoader.getPlugin(pluginClassName);
+
+            if (pd == null)
+            {
+                // status.setValue("Couldn't find plugin online !");
+                throw e;
+            }
+
+            // status.setValue("Installing " + simpleName + "...");
+
+            PluginInstaller.install(pd, false);
+
+            throw new BlocksReloadedException();
+        }
+
+        return (Class<? extends Block>) clazz;
+    }
+
+    @Override
+    public boolean loadFromXML(Node node)
+    {
+        boolean noWarnings = true;
+
+        Element blockNode = (Element) node;
+
+        String blockType = XMLUtil.getAttributeValue(blockNode, "blockType", null);
+
+        try
+        {
+            Class<? extends Block> blockClass = installRequiredBlock(
+                    XMLUtil.getAttributeValue(blockNode, "className", null), blockType);
+
+            block = blockClass.newInstance();
+
+            if (block == null)
+                throw new BlocksException("Couldn't create block from class " + blockClass.getName(), true);
+
+            block.declareInput(inputVars);
+            block.declareOutput(outputVars);
+
+            setID(XMLUtil.getAttributeIntValue(blockNode, "ID", -1));
+
+            int width = XMLUtil.getAttributeIntValue(blockNode, "width", 500);
+            int height = XMLUtil.getAttributeIntValue(blockNode, "height", 500);
+            setDimension(width, height);
+
+            int xPos = XMLUtil.getAttributeIntValue(blockNode, "xLocation", -1);
+            int yPos = XMLUtil.getAttributeIntValue(blockNode, "yLocation", -1);
+            setLocation(xPos, yPos);
+
+            Element varRoot = XMLUtil.getElement(blockNode, "variables");
+            Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+            for (Element varNode : XMLUtil.getElements(inVarRoot))
+            {
+                String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                Var<?> var = inputVars.get(uid);
+
+                if (var == null)
+                {
+                    if (noWarnings)
+                    {
+                        System.err.println("Error(s) while loading protocol:");
+                        noWarnings = false;
+                    }
+                    System.err.println(new NoSuchVariableException(this, uid).getMessage());
+                    continue;
+                }
+
+                if (var instanceof MutableType)
+                {
+                    String type = XMLUtil.getAttributeValue(varNode, "type", null);
+
+                    if (type != null)
+                    {
+                        if (var instanceof VarMutable)
+                        {
+                            Class<?> mutableType = BlocksML.getPrimitiveType(type);
+
+                            if (mutableType == null)
+                                mutableType = Class.forName(type);
+
+                            ((MutableType) var).setType(mutableType);
+                        }
+                        else if (var instanceof VarMutableArray)
+                        {
+                            ((MutableType) var).setType(Class.forName("[L" + type + ";"));
+                        }
+                    }
+                }
+
+                var.loadFromXML(varNode);
+
+                inputVars.setVisible(var, XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+            }
+
+            Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+            for (Element varNode : XMLUtil.getElements(outVarRoot))
+            {
+                String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                Var<?> var = outputVars.get(uid);
+
+                if (var == null)
+                {
+                    if (noWarnings)
+                    {
+                        System.err.println("Error(s) while loading protocol:");
+                        noWarnings = false;
+                    }
+                    System.err.println(new NoSuchVariableException(this, uid).getMessage());
+                    continue;
+                }
+
+                outputVars.setVisible(var, XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+
+                if (var instanceof MutableType)
+                {
+                    String type = XMLUtil.getAttributeValue(varNode, "type", null);
+                    if (type != null)
+                    {
+                        if (var instanceof VarMutable)
+                        {
+                            Class<?> mutableType = BlocksML.getPrimitiveType(type);
+                            ((MutableType) var).setType(mutableType != null ? mutableType : Class.forName(type));
+                        }
+                        else if (var instanceof VarMutableArray)
+                        {
+                            ((MutableType) var).setType(Class.forName("[L" + type + ";"));
+                        }
+                    }
+                }
+            }
+
+        }
+        catch (ClassNotFoundException e1)
+        {
+            throw new BlocksException("Cannot create block (" + e1.getMessage() + ") => class not found", true);
+        }
+        catch (InstantiationException e)
+        {
+            e.printStackTrace();
+        }
+        catch (IllegalAccessException e)
+        {
+            e.printStackTrace();
+        }
+
+        return noWarnings;
+    }
+
+    @Override
+    public boolean saveToXML(Node node)
+    {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public void setDefinedName(String s)
+    {
+        definedName = s;
+    }
+
+    public String getDefinedName()
+    {
+        if (definedName == null || definedName.isEmpty())
+            return getName();
+
+        return definedName;
+    }
+
+    private BlockDescriptor getBlockDescriptor(int blockId, Set<BlockDescriptor> bds)
+    {
+        for (BlockDescriptor bd : bds)
+            if (bd.getID().intValue() == blockId)
+                return bd;
+
+        return null;
+    }
+
+    private String getNewVarId(String oldVarId, Map<BlockDescriptor, BlockDescriptor> copies)
+    {
+        // search for workflow shadow exposed variable
+        final int index = oldVarId.indexOf(":");
+
+        // direct variable id ? --> return it
+        if (index == -1)
+            return oldVarId;
+
+        String result = "";
+
+        // get block Id
+        final int blockId = StringUtil.parseInt(oldVarId.substring(0, index), 0);
+        // get block descriptor from id
+        final BlockDescriptor bd = getBlockDescriptor(blockId, copies.keySet());
+        // get corresponding new block
+        final BlockDescriptor nbd = copies.get(bd);
+
+        if (nbd != null)
+            result = nbd.getID().toString();
+
+        // remaining var id
+        return result + ":" + getNewVarId(oldVarId.substring(index + 1), copies);
+    }
+
+    public BlockDescriptor clone(boolean embedding)
+    {
+        return clone(embedding, new HashMap<BlockDescriptor, BlockDescriptor>());
+    }
+
+    @SuppressWarnings("unchecked")
+    public BlockDescriptor clone(boolean embedding, Map<BlockDescriptor, BlockDescriptor> copies)
+    {
+        Class<? extends Block> blockClass = PluginLoader.getPlugin(getBlock().getClass().getName()).getPluginClass()
+                .asSubclass(Block.class);
+        WorkFlow wf = null;
+        WorkFlow wfCpy = null;
+        BlockDescriptor cpy = null;
+        Block newBlock = null;
+
+        try
+        {
+            newBlock = blockClass.newInstance();
+        }
+        catch (InstantiationException e1)
+        {
+            e1.printStackTrace();
+        }
+        catch (IllegalAccessException e1)
+        {
+            e1.printStackTrace();
+        }
+
+        cpy = (newBlock instanceof WorkFlow ? ((WorkFlow) newBlock).getBlockDescriptor()
+                : new BlockDescriptor(-1, newBlock));
+
+        cpy.setDefinedName(getDefinedName());
+        cpy.setLocation(getLocation().x + 12, getLocation().y + 12);
+        cpy.setDimension(getDimension().width, getDimension().height);
+        cpy.setCollapsed(isCollapsed());
+
+        // Clone input variables
+        for (Var oldVar : inputVars)
+        {
+            final String oldID = inputVars.getID(oldVar);
+
+            // FIX: shadow exposed variable from workflow ? --> don't copy it for now (Stephane)
+            if (oldID.contains(":") && (getBlock() instanceof WorkFlow))
+                continue;
+
+            Var newVar = cpy.inputVars.get(oldID);
+            // If newVar is null, then oldVar was a runtime variable
+            if (newVar == null)
+            {
+                newVar = new VarMutable(oldVar.getName(), oldVar.getType());
+                cpy.inputVars.addRuntimeVariable(oldID, (VarMutable) newVar);
+            }
+            else
+            {
+                if (newVar instanceof VarMutable)
+                    ((VarMutable) newVar).setType(oldVar.getType());
+                newVar.setValue(oldVar.getValue());
+            }
+
+            // FIX: preserve visibility information (Stephane)
+            cpy.inputVars.setVisible(newVar, inputVars.isVisible(oldVar));
+        }
+
+        // Clone output variables
+        for (Var oldVar : outputVars)
+        {
+            final String oldID = outputVars.getID(oldVar);
+
+            // FIX: shadow exposed variable from workflow ? --> don't copy it for now (Stephane)
+            if (oldID.contains(":") && (getBlock() instanceof WorkFlow))
+                continue;
+
+            Var newVar = cpy.outputVars.get(oldID);
+            // If newVar is null, then oldVar was a runtime variable
+            if (newVar == null)
+            {
+                newVar = new VarMutable(oldVar.getName(), oldVar.getType());
+                cpy.outputVars.addRuntimeVariable(oldID, (VarMutable) newVar);
+            }
+            else
+            {
+                if (newVar instanceof VarMutable)
+                    ((VarMutable) newVar).setType(oldVar.getType());
+                newVar.setValue(oldVar.getValue());
+            }
+
+            // FIX: preserve visibility information (Stephane)
+            cpy.outputVars.setVisible(newVar, outputVars.isVisible(oldVar));
+        }
+
+        if (getBlock() instanceof WorkFlow)
+        {
+            wf = (WorkFlow) getBlock();
+            wfCpy = (WorkFlow) cpy.getBlock();
+
+            BlockDescriptor tmp;
+            if (wf.getBlockSelection().isEmpty() || embedding)
+            {
+                for (BlockDescriptor bd : wf)
+                {
+                    tmp = bd.clone(true, copies);
+                    copies.put(bd, tmp);
+                    wfCpy.addBlock(tmp);
+                    wfCpy.selectBlock(tmp);
+                }
+                cloneLinks(wf.getLinksIterator(), copies, wfCpy);
+            }
+            else
+            {
+                for (BlockDescriptor bd : wf.getBlockSelection())
+                {
+                    tmp = bd.clone(true, copies);
+                    copies.put(bd, tmp);
+                    wfCpy.addBlock(tmp);
+                    wfCpy.selectBlock(tmp);
+                }
+                cloneLinks(wf.getLinkSelection(), copies, wfCpy);
+            }
+
+            for (Var oldVar : inputVars)
+            {
+                final String oldID = inputVars.getID(oldVar);
+                final int index = oldID.indexOf(":");
+
+                // shadow exposed variable ? --> try to recover it
+                if (index != -1)
+                {
+                    // get block ID
+                    final int blockID = StringUtil.parseInt(oldID.substring(0, index), 0);
+                    // get var ID
+                    final String varID = oldID.substring(index + 1);
+                    // get new var ID
+                    final String newVarID = getNewVarId(varID, copies);
+                    // get block descriptor from ID
+                    final BlockDescriptor bd = wf.getBlockByID(blockID);
+                    // get corresponding new block
+                    final BlockDescriptor nbd = copies.get(bd);
+                    // get corresponding var for this block
+                    final Var<Object> inputVar = nbd.inputVars.get(newVarID);
+
+                    // finally add the shadow exposed variable if not already done (can be done in WorkFlow.addBlock(..) method) !
+                    if (inputVar != null)
+                    {
+                        if (!cpy.inputVars.contains(inputVar))
+                            cpy.addInput(wfCpy.getInputVarID(inputVar), inputVar);
+                        // and preserve visibility (very important)
+                        cpy.inputVars.setVisible(inputVar, inputVars.isVisible(oldVar));
+                    }
+                }
+            }
+
+            for (Var oldVar : outputVars)
+            {
+                final String oldID = outputVars.getID(oldVar);
+                final int index = oldID.indexOf(":");
+
+                // shadow exposed variable ? --> try to recover it
+                if (index != -1)
+                {
+                    // get block ID
+                    final int blockID = StringUtil.parseInt(oldID.substring(0, index), 0);
+                    // get var ID
+                    final String varID = oldID.substring(index + 1);
+                    // get new var ID
+                    final String newVarID = getNewVarId(varID, copies);
+                    // get block descriptor from ID
+                    final BlockDescriptor bd = wf.getBlockByID(blockID);
+                    // get corresponding new block
+                    final BlockDescriptor nbd = copies.get(bd);
+                    // get corresponding var for this block
+                    final Var<Object> outputVar = nbd.outputVars.get(newVarID);
+
+                    // finally add the shadow exposed variable if not already done (can be done in WorkFlow.addBlock(..) method) !
+                    if (outputVar != null)
+                    {
+                        if (!cpy.outputVars.contains(outputVar))
+                            cpy.addOutput(wfCpy.getInputVarID(outputVar), outputVar);
+                        // and preserve visibility (very important)
+                        cpy.outputVars.setVisible(outputVar, outputVars.isVisible(oldVar));
+                    }
+                }
+            }
+        }
+
+        return cpy;
+    }
+
+    private static void cloneLinks(Iterable<Link<?>> iterable, Map<BlockDescriptor, BlockDescriptor> copies,
+            WorkFlow dest)
+    {
+        for (Link<?> l : iterable)
+        {
+            if (l.srcBlock.getBlock() instanceof Loop || l.dstBlock.getBlock() instanceof Loop)
+            {
+                System.err.println("Warning : cannot copy a link to a loop variable");
+                continue;
+            }
+            if (l.srcBlock.getBlock() instanceof WorkFlow || l.dstBlock.getBlock() instanceof WorkFlow)
+            {
+                System.err.println("Warning : cannot copy a link to an exposed variable");
+                continue;
+            }
+            try
+            {
+                dest.addLink(copies.get(l.srcBlock),
+                        copies.get(l.srcBlock).inputVars.get(l.srcBlock.inputVars.getID(l.srcVar)),
+                        copies.get(l.dstBlock),
+                        copies.get(l.dstBlock).inputVars.get(l.dstBlock.inputVars.getID(l.dstVar)));
+            }
+            catch (NoSuchVariableException nsve)
+            {
+                dest.addLink(copies.get(l.srcBlock),
+                        copies.get(l.srcBlock).outputVars.get(l.srcBlock.outputVars.getID(l.srcVar)),
+                        copies.get(l.dstBlock),
+                        copies.get(l.dstBlock).inputVars.get(l.dstBlock.inputVars.getID(l.dstVar)));
+            }
+        }
+    }
+
+    /**
+     * @return <code>true</code> if the block keeps results in memory, <code>false</code> otherwise
+     */
+    public boolean keepsResults()
+    {
+        return keepResults;
+    }
+
+    /**
+     * Sets whether the block should keep results in memory (and not need to recalculate it unless a
+     * parameter has changed)
+     * 
+     * @param keep
+     *        Flag to keep results in memory.
+     */
+    public void keepResults(boolean keep)
+    {
+        this.keepResults = keep;
+    }
+
+    /**
+     * (This method is used only if the block implements {@link InputBlock})
+     * 
+     * @return the command-line identifier for the input block's variable
+     */
+    public String getCommandLineID()
+    {
+        return commandLineID;
+    }
+
+    /**
+     * (This method is used only if the block implements {@link InputBlock})
+     * Sets the command line identifier for this input block's variable
+     * 
+     * @param id
+     *        Block identifier when using the command line.
+     */
+    public void setCommandLineID(String id)
+    {
+        commandLineID = id;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/FileBatch.java b/src/main/java/plugins/adufour/blocks/lang/FileBatch.java
new file mode 100644
index 0000000000000000000000000000000000000000..827bb51bfa31ed079643ca5162daf0a0c9997af6
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/FileBatch.java
@@ -0,0 +1,130 @@
+package plugins.adufour.blocks.lang;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.List;
+
+import icy.file.FileUtil;
+import icy.gui.frame.progress.AnnounceFrame;
+import icy.main.Icy;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeModel;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarBoolean;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarString;
+import plugins.adufour.vars.util.VarException;
+
+/**
+ * A file batch is a work-flow that will execute repeatedly on every file of a
+ * user-selected folder. Files can be retrieved from sub-folders if necessary,
+ * and filtered by extension. Note however that the files are given "as-is": it
+ * is up to the user to indicate inside the work-flow how these files should be
+ * read (for the particular case of image files, use the
+ * {@link SequenceFileBatch Sequence file batch} instead).
+ * 
+ * @author Alexandre Dufour
+ */
+public class FileBatch extends Batch implements FileFilter
+{
+    // loop variables //
+
+    private VarFile element;
+
+    private VarFile folder;
+
+    private VarString extension;
+
+    private VarBoolean includeSubFolders;
+
+    // local variables //
+
+    /**
+     * The list of files containing the elements to process
+     */
+    protected File[] files;
+
+    @Override
+    public Var<?> getBatchSource()
+    {
+        if (folder == null)
+        {
+            // WARNING: do *not* change the name of this variable
+            // why? see declareInput() and VarList.add()
+            folder = new VarFile("folder", null);
+            folder.setDefaultEditorModel(new FileTypeModel(null, FileMode.FOLDERS, null, false));
+        }
+        return folder;
+    }
+
+    @Override
+    public boolean accept(File f)
+    {
+        String ext = extension.getValue();
+        return f.isDirectory() || ext.isEmpty() || f.getPath().toLowerCase().endsWith(ext.toLowerCase());
+    }
+
+    @Override
+    public Var<?> getBatchElement()
+    {
+        if (element == null)
+        {
+            // WARNING: do *not* change the name of this variable
+            // why? see declareInput() and VarList.add()
+            element = new VarFile("file", null);
+        }
+        return element;
+    }
+
+    @Override
+    public void initializeLoop()
+    {
+        if (folder.getValue() == null)
+            throw new VarException(folder, "No folder indicated");
+
+        File file = folder.getValue();
+        if (!file.isDirectory())
+            throw new VarException(folder, file.getAbsolutePath() + " is not a folder");
+
+        AnnounceFrame process = null;
+
+        if (!Icy.getMainInterface().isHeadLess())
+            process = new AnnounceFrame("Listing files...");
+
+        files = FileUtil.getFiles(file, this, includeSubFolders.getValue(), false, false);
+
+        if (process != null)
+            process.close();
+    }
+
+    @Override
+    public void beforeIteration()
+    {
+        element.setValue(files[getIterationCounter().getValue()]);
+    }
+
+    @Override
+    public boolean isStopConditionReached()
+    {
+        return getIterationCounter().getValue() == files.length;
+    }
+
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        super.declareInput(inputMap);
+
+        inputMap.add("extension", extension = new VarString("extension", ""));
+        inputMap.add("Include sub-folders", includeSubFolders = new VarBoolean("Include sub-folders", true));
+    }
+
+    @Override
+    public void declareLoopVariables(List<Var<?>> loopVariables)
+    {
+        super.declareLoopVariables(loopVariables);
+
+        loopVariables.add(extension);
+        loopVariables.add(includeSubFolders);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/FolderLoop.java b/src/main/java/plugins/adufour/blocks/lang/FolderLoop.java
new file mode 100644
index 0000000000000000000000000000000000000000..c02f5d306a1a4726be63facbec02c26031b74fec
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/FolderLoop.java
@@ -0,0 +1,9 @@
+package plugins.adufour.blocks.lang;
+
+/**
+ * @deprecated use {@link FileBatch} instead.
+ * @author Alexandre Dufour
+ */
+@Deprecated
+public class FolderLoop extends FileBatch
+{}
diff --git a/src/main/java/plugins/adufour/blocks/lang/Link.java b/src/main/java/plugins/adufour/blocks/lang/Link.java
new file mode 100644
index 0000000000000000000000000000000000000000..26cb75514be76d9566b702f77e6f749520998b7d
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/Link.java
@@ -0,0 +1,126 @@
+package plugins.adufour.blocks.lang;
+
+import icy.file.xml.XMLPersistent;
+import icy.util.XMLUtil;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import plugins.adufour.blocks.util.BlocksException;
+import plugins.adufour.blocks.util.BlocksML;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarMutableArray;
+import plugins.adufour.vars.util.MutableType;
+
+/**
+ * Class describing a link between two variables within a work flow
+ * 
+ * @author Alexandre Dufour
+ */
+public class Link<T> implements XMLPersistent
+{
+    private final WorkFlow workFlow;
+
+    public BlockDescriptor srcBlock;
+
+    public Var<T> srcVar;
+
+    public BlockDescriptor dstBlock;
+
+    public Var<T> dstVar;
+
+    /**
+     * Creates a new (empty) link in the specified work flow. This constructor is generally followed
+     * by a call to {@link #loadFromXML(Node)} to restore the link status from a previously saved
+     * state (e.g. XML file)
+     * 
+     * @param workFlow
+     *        Workflow containing the new link.
+     */
+    public Link(WorkFlow workFlow)
+    {
+        this.workFlow = workFlow;
+    }
+
+    public Link(WorkFlow workFlow, final BlockDescriptor srcBlock, final Var<T> output, final BlockDescriptor dstBlock,
+            final Var<T> input)
+    {
+        this(workFlow);
+
+        this.srcBlock = srcBlock;
+        this.srcVar = output;
+        this.dstBlock = dstBlock;
+        this.dstVar = input;
+    }
+
+    public Class<?> getType()
+    {
+        return srcVar.getType();
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    @Override
+    public boolean loadFromXML(Node node)
+    {
+        Element linkNode = (Element) node;
+
+        int srcBlockID = XMLUtil.getAttributeIntValue(linkNode, "srcBlockID", -1);
+        String srcVarID = XMLUtil.getAttributeValue(linkNode, "srcVarID", null);
+        int dstBlockID = XMLUtil.getAttributeIntValue(linkNode, "dstBlockID", -1);
+        String dstVarID = XMLUtil.getAttributeValue(linkNode, "dstVarID", null);
+
+        // load the source variable
+        BlockDescriptor theSrcBlock = workFlow.getBlockByID(srcBlockID);
+        Var theSrcVar = theSrcBlock.outputVars.get(srcVarID);
+        if (theSrcVar == null)
+            theSrcVar = theSrcBlock.inputVars.get(srcVarID);
+
+        if (theSrcVar == null)
+        {
+            System.err.println("Cannot create a link from variable " + srcVarID + " (from block " + theSrcBlock + ")");
+            return false;
+        }
+
+        if (theSrcVar instanceof MutableType)
+        {
+            String type = XMLUtil.getAttributeValue(linkNode, "srcVarType", null);
+            if (type != null)
+            {
+                try
+                {
+                    if (theSrcVar instanceof VarMutable)
+                    {
+                        Class<?> mutableType = BlocksML.getPrimitiveType(type);
+                        ((MutableType) theSrcVar).setType(mutableType != null ? mutableType : Class.forName(type));
+                    }
+                    else if (theSrcVar instanceof VarMutableArray)
+                    {
+                        type = "[L" + type + ";";
+                        ((MutableType) theSrcVar).setType(Class.forName(type));
+                    }
+                }
+                catch (ClassNotFoundException e1)
+                {
+                    throw new BlocksException("Cannot create link: unknown type " + type, true);
+                }
+            }
+        }
+
+        // load the destination variable
+        BlockDescriptor theDstBlock = workFlow.getBlockByID(dstBlockID);
+
+        Var theDstVar = theDstBlock.inputVars.get(dstVarID);
+
+        workFlow.addLink(theSrcBlock, theSrcVar, theDstBlock, theDstVar);
+
+        return true;
+    }
+
+    @Override
+    public boolean saveToXML(Node node)
+    {
+        // TODO Auto-generated method stub
+        return false;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/Loop.java b/src/main/java/plugins/adufour/blocks/lang/Loop.java
new file mode 100644
index 0000000000000000000000000000000000000000..b3edd5bbed6d93b6827eee0b3ec64d7750c3d01a
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/Loop.java
@@ -0,0 +1,278 @@
+package plugins.adufour.blocks.lang;
+
+import icy.system.IcyHandledException;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import plugins.adufour.blocks.lang.BlockDescriptor.BlockStatus;
+import plugins.adufour.blocks.tools.ReLoop;
+import plugins.adufour.blocks.util.BlocksException;
+import plugins.adufour.blocks.util.LoopException;
+import plugins.adufour.blocks.util.ScopeException;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarBoolean;
+import plugins.adufour.vars.lang.VarInteger;
+
+/**
+ * Special work flow that will repeat its contents forever until is it manually stopped by the user.
+ * 
+ * @see Batch
+ * @see RangeLoop
+ * @see FileBatch
+ * @see SequenceFileBatch
+ * @see SequenceSeriesBatch
+ * @author Alexandre Dufour
+ */
+public class Loop extends WorkFlow
+{
+    private final ArrayList<Var<?>> loopVariables = new ArrayList<Var<?>>();
+
+    private VarInteger iterationCounter;
+
+    public final VarBoolean stopOnFirstError = new VarBoolean("Stop on first error", false);
+
+    public Loop()
+    {
+        super(false);
+
+        declareLoopVariables(loopVariables);
+
+        // always add the iteration counter in last position
+        loopVariables.add(iterationCounter);
+    }
+
+    @Override
+    public BlockDescriptor getBlock(int blockID)
+    {
+        if (blockID == -1)
+            return getBlockDescriptor();
+
+        return super.getBlock(blockID);
+    }
+
+    @Override
+    public BlockDescriptor getInputOwner(Var<?> var)
+    {
+        return loopVariables.contains(var) ? getBlockDescriptor() : super.getInputOwner(var);
+    }
+
+    @Override
+    public BlockDescriptor getOutputOwner(Var<?> var)
+    {
+        return loopVariables.contains(var) ? getBlockDescriptor() : super.getOutputOwner(var);
+    }
+
+    public String getInputVarID(Var<?> variable)
+    {
+        for (Var<?> innerVar : loopVariables)
+            if (variable == innerVar)
+            {
+                return getBlockDescriptor().inputVars.getID(variable);
+            }
+
+        return super.getInputVarID(variable);
+    }
+
+    public String getOutputVarID(Var<?> variable)
+    {
+        for (Var<?> innerVar : loopVariables)
+            if (variable == innerVar)
+            {
+                return getBlockDescriptor().outputVars.getID(variable);
+            }
+
+        return super.getOutputVarID(variable);
+    }
+
+    public VarInteger getIterationCounter()
+    {
+        return iterationCounter;
+    }
+
+    /**
+     * Adds the specified variable to the list of inner loop variables, i.e., variables describing
+     * the parameters of the loop, which can be referenced from inside the loop by other blocks.
+     * 
+     * @deprecated The process of declaring loop variables has changed. Override
+     *             {@link #declareLoopVariables(List)} instead.
+     * @param loopVar
+     *        the variable to register
+     */
+    @Deprecated
+    protected void addLoopVariable(Var<?> loopVar)
+    {
+        loopVariables.add(loopVar);
+    }
+
+    /**
+     * @param var
+     *        the variable to look for
+     * @return true if <code>var</code> is a loop variable, false otherwise
+     */
+    public boolean isLoopVariable(Var<?> var)
+    {
+        return loopVariables.contains(var);
+    }
+
+    public List<Var<?>> getLoopVariables()
+    {
+        return loopVariables;
+    }
+
+    @Override
+    public void run()
+    {
+        iterationCounter.setValue(0);
+        initializeLoop();
+
+        List<IcyHandledException> exceptions = new ArrayList<IcyHandledException>();
+
+        while (!Thread.currentThread().isInterrupted() && !isStopConditionReached())
+        {
+            // FIXED (Stephane)
+            // we need to reset to dirty state for all contained blocks before the iteration
+            // as we never expect any contained block to keep value from previous iteration
+            final Iterator<BlockDescriptor> blockIt = iterator();
+
+            while (blockIt.hasNext())
+            {
+                final BlockDescriptor block = blockIt.next();
+                // put block back in dirty state
+                if (block.getStatus() == BlockStatus.READY)
+                    block.setStatus(BlockStatus.DIRTY);
+            }
+
+            beforeIteration();
+
+            try
+            {
+                super.run();
+            }
+            catch (IcyHandledException e)
+            {
+                exceptions.add(e);
+                if (stopOnFirstError.getValue())
+                    break;
+            }
+
+            iterationCounter.setValue(iterationCounter.getValue() + 1);
+            afterIteration();
+        }
+
+        if (exceptions.size() > 0)
+        {
+            String message = "The following errors occurred during the loop:\n\n";
+            for (Exception e : exceptions)
+                message += " - " + e.getMessage() + "\n";
+            throw new BlocksException(message, true);
+        }
+    }
+
+    /**
+     * Initializes (or resets) internal loop structures such as iterators or counters. This method
+     * is called once before actually starting the loop. Note that the iteration counter is already
+     * reset before calling this method.
+     */
+    public void initializeLoop()
+    {
+        // nothing to do in an infinite loop
+    }
+
+    /**
+     * Called before the current iteration takes place (i.e. before calling the {@link #run()}
+     * method. This method is typically used to fetch array or iterator values necessary for the
+     * core code execution.
+     */
+    public void beforeIteration()
+    {
+        // nothing to do in an infinite loop
+    }
+
+    /**
+     * Notifies that the current iteration has finished, allowing the loop to handle custom
+     * increments or iterators to move forward before executing the next iteration. Note that the
+     * iteration counter is automatically increased before calling this method.
+     */
+    public void afterIteration()
+    {
+        // release other threads for a very short time
+        // (especially the AWT, to receive user interruption and display stuff)
+        Thread.yield();
+
+    }
+
+    /**
+     * NB: this method must be called by overriding methods to ensure that the loop can be
+     * interrupted correctly upon request
+     * 
+     * @return true if the stopping condition is reached, false otherwise.
+     */
+    public boolean isStopConditionReached()
+    {
+        // infinite loop
+        return false;
+    }
+
+    @Override
+    protected <T> void checkScope(Link<T> link) throws ScopeException
+    {
+        try
+        {
+            super.checkScope(link);
+        }
+        catch (ScopeException e)
+        {
+            // authorize links coming from variable loops
+            if (!loopVariables.contains(link.srcVar))
+                throw e;
+        }
+    }
+
+    @Override
+    protected <T> void checkLoop(Link<T> link) throws LoopException
+    {
+        try
+        {
+            super.checkLoop(link);
+        }
+        catch (LoopException e)
+        {
+            // authorize loops only if the destination is a "ReLoop" block
+            if (link.dstBlock.getBlock() instanceof ReLoop)
+            {
+                ReLoop reLoop = (ReLoop) link.dstBlock.getBlock();
+
+                if (link.dstVar == reLoop.reloopValue)
+                    return;
+            }
+
+            throw e;
+        }
+    }
+
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        super.declareOutput(outputMap);
+
+        iterationCounter = new VarInteger("iteration", 0);
+        iterationCounter.setEnabled(false);
+        outputMap.add("iteration", iterationCounter);
+    }
+
+    /**
+     * Declares the necessary loop variables by adding them to the specified list. Loop variables
+     * describe the parameters of the loop, which can be referenced from inside the loop by other
+     * blocks.
+     * 
+     * @param loopVars
+     *        the variable to register
+     */
+    public void declareLoopVariables(List<Var<?>> loopVars)
+    {
+        // a loop has no inner variable
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/RangeLoop.java b/src/main/java/plugins/adufour/blocks/lang/RangeLoop.java
new file mode 100644
index 0000000000000000000000000000000000000000..cf75e202d6f98dd790e0cbabc5442278c47a146b
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/RangeLoop.java
@@ -0,0 +1,98 @@
+package plugins.adufour.blocks.lang;
+
+import java.util.List;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarInteger;
+
+/**
+ * Particular type of loop that will repeats its contents for every number within the provided range
+ * of values
+ * 
+ * @author Alexandre Dufour
+ */
+public class RangeLoop extends Loop
+{
+    private VarInteger startIndex;
+    private VarInteger endIndex;
+    private VarInteger step;
+    private VarInteger index;
+    
+    public VarInteger getEndIndex()
+    {
+        return endIndex;
+    }
+    
+    public VarInteger getIndex()
+    {
+        return index;
+    }
+    
+    public VarInteger getStartIndex()
+    {
+        return startIndex;
+    }
+    
+    public VarInteger getStep()
+    {
+        return step;
+    }
+    
+    @Override
+    public void initializeLoop()
+    {
+        index.setValue(startIndex.getValue());
+    }
+    
+    @Override
+    public void beforeIteration()
+    {
+        // nothing to do here
+    }
+    
+    @Override
+    public void afterIteration()
+    {
+        index.setValue(index.getValue() + step.getValue());
+    }
+    
+    @Override
+    public boolean isStopConditionReached()
+    {
+        if (super.isStopConditionReached()) return true;
+        
+        if (step.getValue() > 0) return index.getValue() >= endIndex.getValue();
+        
+        return index.getValue() <= endIndex.getValue();
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        super.declareInput(inputMap);
+        
+        inputMap.add("start", startIndex = new VarInteger("start", 0));
+        inputMap.add("step", step = new VarInteger("step", 1));
+        inputMap.add("end", endIndex = new VarInteger("end", 10));
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        super.declareOutput(outputMap);
+        
+        outputMap.add("index", index = new VarInteger("index", startIndex.getDefaultValue().intValue()));
+    }
+    
+    @Override
+    public void declareLoopVariables(List<Var<?>> loopVariables)
+    {
+        loopVariables.add(startIndex);
+        loopVariables.add(endIndex);
+        loopVariables.add(step);
+        loopVariables.add(index);
+        
+        super.declareLoopVariables(loopVariables);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/plugins/adufour/blocks/lang/SequenceFileBatch.java b/src/main/java/plugins/adufour/blocks/lang/SequenceFileBatch.java
new file mode 100644
index 0000000000000000000000000000000000000000..c58c047c4b7b4e77b2037c0719bb79ac4ed59d6b
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/SequenceFileBatch.java
@@ -0,0 +1,56 @@
+package plugins.adufour.blocks.lang;
+
+import java.io.File;
+
+import icy.file.Loader;
+import plugins.adufour.vars.lang.VarSequence;
+
+/**
+ * Similar to the {@link FileBatch File batch}, a Sequence file batch will read
+ * all files of a user-defined folder, and additionally load and give access to
+ * the sequence contained within these files (non-imaging files will be
+ * skipped).<br>
+ * Note for multi-series imaging files (e.g. Leica .lif): when loading
+ * multi-series files, a dialog box will appear to let the user select the
+ * series to process. However only the first selected series will be loaded. To
+ * batch process entire multi-series files, use the {@link SequenceSeriesBatch
+ * Sequence series batch} instead.
+ * 
+ * @author Alexandre Dufour
+ */
+public class SequenceFileBatch extends FileBatch
+{
+    VarSequence element;
+
+    @Override
+    public VarSequence getBatchElement()
+    {
+        if (element == null)
+        {
+            // WARNING: do *not* change the name of this variable
+            // why? see declareInput() and VarList.add()
+            element = new VarSequence("Sequence", null);
+        }
+        return element;
+    }
+
+    @Override
+    public boolean accept(File f)
+    {
+        return f.isDirectory() || super.accept(f) && Loader.isSupportedImageFile(f.getPath());
+    }
+
+    @Override
+    public void beforeIteration()
+    {
+        try
+        {
+            element.setValue(Loader.loadSequence(files[getIterationCounter().getValue()].getPath(), -1, false));
+            Thread.yield();
+        }
+        catch (Exception e)
+        {
+            Thread.currentThread().interrupt();
+        }
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/SequenceSeriesBatch.java b/src/main/java/plugins/adufour/blocks/lang/SequenceSeriesBatch.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b549f56d92051bea01dc8f8abe144ca6815357a
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/SequenceSeriesBatch.java
@@ -0,0 +1,110 @@
+package plugins.adufour.blocks.lang;
+
+import java.io.File;
+import java.io.IOException;
+
+import icy.common.exception.UnsupportedFormatException;
+import icy.file.Loader;
+import icy.file.SequenceFileImporter;
+import icy.sequence.MetaDataUtil;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeModel;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarSequence;
+import plugins.adufour.vars.util.VarException;
+import plugins.kernel.importer.LociImporterPlugin;
+
+/**
+ * A Sequence series batch will iteratively load all series contained in a multi-series (e.g. Leica
+ * .lif) file, allowing to process all the series with a single work-flow. It can be used in
+ * combination with the {@link FileBatch File batch} to batch process entire folders of multi-series
+ * files.
+ * 
+ * @author Alexandre Dufour
+ */
+public class SequenceSeriesBatch extends Batch
+{
+    private VarFile multiSeriesFile;
+
+    private int nbSeries;
+
+    private VarSequence element;
+
+    @Override
+    public VarFile getBatchSource()
+    {
+        if (multiSeriesFile == null)
+        {
+            // WARNING: do *not* change the name of this variable
+            // why? see declareInput() and VarList.add()
+            multiSeriesFile = new VarFile("Series file", null);
+            multiSeriesFile.setDefaultEditorModel(new FileTypeModel(null, FileMode.FILES, null, false));
+        }
+
+        return multiSeriesFile;
+    }
+
+    @Override
+    public VarSequence getBatchElement()
+    {
+        if (element == null)
+        {
+            // WARNING: do *not* change the name of this variable
+            // why? see declareInput() and VarList.add()
+            element = new VarSequence("Sequence", null);
+        }
+
+        return element;
+    }
+
+    @Override
+    public void initializeLoop()
+    {
+        if (multiSeriesFile.getValue() == null)
+            throw new VarException(multiSeriesFile, "No file indicated");
+
+        File f = multiSeriesFile.getValue();
+        if (f.isDirectory())
+            throw new VarException(multiSeriesFile, f.getAbsolutePath() + " is not a file");
+
+        String path = f.getPath();
+
+        try
+        {
+            final SequenceFileImporter importer = Loader.getSequenceFileImporter(path, true);
+
+            // FIX: force un-grouping to get number of series (Stephane)
+            if (importer instanceof LociImporterPlugin)
+                ((LociImporterPlugin) importer).setGroupFiles(false);
+
+            nbSeries = MetaDataUtil.getNumSeries(Loader.getOMEXMLMetaData(importer, path));
+        }
+        catch (UnsupportedFormatException e)
+        {
+            throw new VarException(multiSeriesFile, path + " is not an imaging file");
+        }
+        catch (IOException e)
+        {
+            throw new VarException(multiSeriesFile, "Unable to read " + path);
+        }
+    }
+
+    @Override
+    public void beforeIteration()
+    {
+        final String path = multiSeriesFile.getValue().getPath();
+        final SequenceFileImporter importer = Loader.getSequenceFileImporter(path, true);
+
+        // FIX: force un-grouping as we iterate over series (Stephane)
+        if (importer instanceof LociImporterPlugin)
+            ((LociImporterPlugin) importer).setGroupFiles(false);
+
+        element.setValue(Loader.loadSequence(importer, path, getIterationCounter().getValue(), true));
+    }
+
+    @Override
+    public boolean isStopConditionReached()
+    {
+        return getIterationCounter().getValue() == nbSeries;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/lang/WorkFlow.java b/src/main/java/plugins/adufour/blocks/lang/WorkFlow.java
new file mode 100644
index 0000000000000000000000000000000000000000..499ac7d95c06b0d348339e73723da2361b39a918
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/lang/WorkFlow.java
@@ -0,0 +1,1175 @@
+package plugins.adufour.blocks.lang;
+
+import java.awt.Point;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import icy.gui.dialog.ConfirmDialog;
+import icy.main.Icy;
+import icy.math.UnitUtil;
+import icy.plugin.abstract_.Plugin;
+import icy.system.IcyHandledException;
+import icy.system.thread.ThreadUtil;
+import icy.util.StringUtil;
+import plugins.adufour.blocks.lang.BlockDescriptor.BlockStatus;
+import plugins.adufour.blocks.util.BlockListener;
+import plugins.adufour.blocks.util.BlocksException;
+import plugins.adufour.blocks.util.LinkCutException;
+import plugins.adufour.blocks.util.LoopException;
+import plugins.adufour.blocks.util.NoSuchBlockException;
+import plugins.adufour.blocks.util.NoSuchLinkException;
+import plugins.adufour.blocks.util.NoSuchVariableException;
+import plugins.adufour.blocks.util.ScopeException;
+import plugins.adufour.blocks.util.StopException;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.blocks.util.VarListListener;
+import plugins.adufour.blocks.util.WorkFlowListener;
+import plugins.adufour.protocols.gui.MainFrame;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarObject;
+import plugins.adufour.vars.util.VarException;
+
+public class WorkFlow extends Plugin implements Block, Iterable<BlockDescriptor>, BlockListener, WorkFlowListener
+{
+    private final BlockDescriptor descriptor = new BlockDescriptor(-1, this);
+
+    private final HashSet<WorkFlowListener> listeners = new HashSet<WorkFlowListener>();
+
+    private final ArrayList<BlockDescriptor> orderedBlocks = new ArrayList<BlockDescriptor>();
+
+    private final boolean topLevel;
+
+    private final ArrayList<Link<?>> links = new ArrayList<Link<?>>();
+
+    private final VarListListener inputVarListener = new VarListListener()
+    {
+        @Override
+        public void variableAdded(VarList list, Var<?> variable)
+        {
+            descriptor.addInput(getInputVarID(variable), variable);
+        }
+
+        @Override
+        public void variableRemoved(VarList list, Var<?> variable)
+        {
+            descriptor.removeInput(variable);
+        }
+    };
+
+    private final VarListListener outputVarListener = new VarListListener()
+    {
+        @Override
+        public void variableAdded(VarList list, Var<?> variable)
+        {
+            descriptor.addOutput(getOutputVarID(variable), variable);
+        }
+
+        @Override
+        public void variableRemoved(VarList list, Var<?> variable)
+        {
+            descriptor.removeOutput(variable);
+        }
+    };
+
+    private Thread executionThread;
+
+    private boolean userInterruption = false;
+
+    private ArrayList<BlockDescriptor> blockSelection = new ArrayList<BlockDescriptor>();
+    private ArrayList<Link<?>> linkSelection = new ArrayList<Link<?>>();
+
+    private static final OutputStream nullStream = new OutputStream()
+    {
+        @Override
+        public void write(int b) throws IOException
+        {
+        }
+    };
+
+    /**
+     * The stream where the execution log is sent.
+     */
+    private PrintStream logStream = new PrintStream(nullStream);
+
+    public void setLogStream(PrintStream logStream)
+    {
+        this.logStream = logStream;
+    }
+
+    /**
+     * Searches recursively for the top-level stream where the execution log will be sent
+     * 
+     * @return The top-level stream where the log is sent
+     */
+    public PrintStream getLogStream()
+    {
+        return descriptor.getContainer() == null ? logStream : descriptor.getContainer().getLogStream();
+    }
+
+    public WorkFlow()
+    {
+        this(false);
+    }
+
+    public WorkFlow(boolean topLevel)
+    {
+        super();
+
+        this.topLevel = topLevel;
+    }
+
+    public void addBlock(BlockDescriptor blockInfo)
+    {
+        blockInfo.setContainer(this);
+
+        orderedBlocks.add(blockInfo);
+        blockInfo.addBlockListener(this);
+
+        if (blockInfo.isWorkFlow())
+            ((WorkFlow) blockInfo.getBlock()).addListener(this);
+
+        // FIX: sometime getContainer() isn't yet initialized even for internal workflow (Stephane)
+        // if (descriptor.getContainer() != null)
+        if (!descriptor.isTopLevelWorkFlow())
+        {
+            // Add the variables from the block to the work flow itself
+            // this is to allow variable exposing
+
+            // Retrieve input variables
+            for (Var<?> inputVar : blockInfo.inputVars)
+            {
+                // FIX: avoid to duplicate variable (Stephane)
+                if (!descriptor.inputVars.contains(inputVar))
+                    descriptor.addInput(getInputVarID(inputVar), inputVar);
+            }
+            // Retrieve output variables
+            for (Var<?> outputVar : blockInfo.outputVars)
+            {
+                // FIX: avoid to duplicate variable (Stephane)
+                if (!descriptor.outputVars.contains(outputVar))
+                    descriptor.addOutput(getOutputVarID(outputVar), outputVar);
+            }
+
+            blockInfo.inputVars.addVarListListener(inputVarListener);
+            blockInfo.outputVars.addVarListListener(outputVarListener);
+        }
+
+        blockAdded(this, blockInfo);
+    }
+
+    /**
+     * @deprecated Use {@link #addBlock(BlockDescriptor)} instead.
+     * @param block
+     *        Block to add.
+     * @return Descriptor relative to this workflow of the added block.
+     */
+    @Deprecated
+    public BlockDescriptor addBlock(Block block)
+    {
+        return addBlock(-1, block, new Point());
+    }
+
+    /**
+     * @deprecated Use {@link #addBlock(BlockDescriptor)} instead.
+     * @param ID
+     *        Block identifier.
+     * @param block
+     *        Block to be added.
+     * @param location
+     *        Position to put the block at.
+     * @return Descriptor relative to this workflow of the added block.
+     */
+    @SuppressWarnings("deprecation")
+    @Deprecated
+    public BlockDescriptor addBlock(int ID, Block block, Point location)
+    {
+        BlockDescriptor blockDescriptor;
+
+        if (block instanceof WorkFlow)
+        {
+            blockDescriptor = ((WorkFlow) block).descriptor;
+            blockDescriptor.setContainer(this);
+            blockDescriptor.setLocation(location.x, location.y);
+        }
+        else
+        {
+            blockDescriptor = new BlockDescriptor(ID, block, this, location);
+        }
+
+        addBlock(blockDescriptor);
+
+        return blockDescriptor;
+    }
+
+    /**
+     * Links the specified variables
+     * 
+     * @param <T>
+     *        Type of the variable to be linked.
+     * @param srcBlock
+     *        the source block
+     * @param srcArgID
+     *        the unique ID of the source (output) variable
+     * @param dstBlock
+     *        the destination block
+     * @param dstArgID
+     *        the unique ID of the destination (input) variable
+     * @return the newly created link
+     * @throws NoSuchBlockException
+     *         if either the source or destination block is not in this work flow
+     * @throws NoSuchVariableException
+     *         if either the source or destination variable is not in the corresponding block
+     */
+    public <T> Link<T> addLink(BlockDescriptor srcBlock, String srcArgID, BlockDescriptor dstBlock, String dstArgID)
+            throws NoSuchBlockException, NoSuchVariableException, ClassCastException
+    {
+        if (!orderedBlocks.contains(srcBlock))
+            throw new NoSuchBlockException(srcBlock);
+        if (!orderedBlocks.contains(dstBlock))
+            throw new NoSuchBlockException(dstBlock);
+
+        Var<T> srcVar = srcBlock.outputVars.get(srcArgID);
+        if (srcVar == null)
+            throw new NoSuchVariableException(srcBlock, srcArgID);
+
+        Var<T> dstVar = dstBlock.inputVars.get(dstArgID);
+        if (dstVar == null)
+            throw new NoSuchVariableException(dstBlock, dstArgID);
+
+        return addLink(srcBlock, srcVar, dstBlock, dstVar);
+    }
+
+    /**
+     * Creates a link between the given variables and re-schedules the blocks accordingly
+     * 
+     * @param <T>
+     *        Type of the value of the link that is generated.
+     * @param srcBlock
+     *        Start-point block descriptor of the link.
+     * @param srcVar
+     *        Start-point variable instance of the link.
+     * @param dstBlock
+     *        End-point block descriptor of the link.
+     * @param dstVar
+     *        End-point variable instance of the link.
+     * @return Link instance connecting source and destination blocks.
+     * @throws LoopException
+     *         if a loop is detected in the current scope
+     * @throws NoSuchVariableException
+     *         if the variable cannot be found in the current scope
+     * @throws ClassCastException
+     *         If the link is not valid, meaning that the two specified variables are of
+     *         incompatible types.
+     */
+    public <T> Link<T> addLink(BlockDescriptor srcBlock, Var<T> srcVar, BlockDescriptor dstBlock, Var<T> dstVar)
+            throws LoopException, ClassCastException
+    {
+        Link<T> link = checkLink(srcBlock, srcVar, dstBlock, dstVar);
+
+        dstVar.setReference(srcVar);
+
+        // re-order the boxes to make sure srcBlock runs before dstBlock
+        if (orderedBlocks.indexOf(link.srcBlock) > orderedBlocks.indexOf(link.dstBlock))
+            reOrder(link.srcBlock, link.dstBlock);
+
+        links.add(link);
+
+        for (WorkFlowListener listener : listeners)
+        {
+            listener.linkAdded(this, link);
+        }
+
+        return link;
+    }
+
+    public void addListener(WorkFlowListener listener)
+    {
+        listeners.add(listener);
+    }
+
+    public boolean isTopLevel()
+    {
+        return topLevel;
+    }
+
+    public boolean contains(BlockDescriptor dstBlock)
+    {
+        return orderedBlocks.contains(dstBlock);
+    }
+
+    /**
+     * Checks whether the specified variables can be linked, and if so returns the pair of
+     * {@link Block} objects to link
+     * 
+     * @param <T>
+     *        Type of value being transported through the link.
+     * @param srcBlock
+     *        the {@link Block} to link from
+     * @param srcVar
+     *        the {@link Var}iable to link from
+     * @param dstBlock
+     *        the {@link Block} to link to
+     * @param dstVar
+     *        the {@link Var}iable to link to
+     * @return The link instance connecting source and destination variables.
+     * @throws ClassCastException
+     *         if the link is not valid, meaning that the two specified variables are of
+     *         incompatible types
+     */
+    public <T> Link<T> checkLink(final BlockDescriptor srcBlock, Var<T> srcVar, final BlockDescriptor dstBlock,
+            Var<T> dstVar) throws ClassCastException
+    {
+        Link<T> link = new Link<T>(this, srcBlock, srcVar, dstBlock, dstVar);
+
+        checkScope(link);
+
+        checkLoop(link);
+
+        checkType(link);
+
+        return link;
+    }
+
+    protected <T> void checkScope(Link<T> link) throws ScopeException
+    {
+        if (contains(link.srcBlock) != contains(link.dstBlock))
+            throw new ScopeException();
+
+        // special case: inner loop variables should not link to outside the loop
+        if (link.srcBlock.isLoop() && ((Loop) link.srcBlock.getBlock()).isLoopVariable(link.srcVar))
+            throw new ScopeException();
+    }
+
+    protected <T> void checkLoop(Link<T> link) throws LoopException
+    {
+        if (link.srcBlock.equals(link.dstBlock) || depends(link.srcBlock, link.dstBlock))
+            throw new LoopException();
+    }
+
+    protected <T> void checkType(Link<T> link) throws ClassCastException
+    {
+        // filter all valid cases
+
+        if (link.dstVar.isAssignableFrom(link.srcVar))
+            return;
+
+        if (link.srcVar instanceof VarObject)
+            return;
+
+        if (link.dstVar instanceof VarMutable)
+            return;
+        // {
+        // VarMutable dst = (VarMutable) link.dstVar;
+        // dst.setType(link.srcVar.getType());
+        // return;
+        // }
+
+        throw new ClassCastException("<html><h4>Variables \"" + link.dstVar.getName() + "\" and \""
+                + link.srcVar.getName() + "\" are of different type and cannot be linked</h4></html>");
+    }
+
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+    }
+
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+
+    private boolean depends(BlockDescriptor srcBlock, BlockDescriptor dstBlock)
+    {
+        boolean srcDependsOnDst = false;
+
+        for (Link<?> link : links)
+        {
+            if (link.dstBlock == srcBlock)
+            {
+                // link points to srcBlock.
+                if (link.srcBlock == dstBlock)
+                    return true;
+
+                // else
+                srcDependsOnDst |= depends(link.srcBlock, dstBlock);
+
+                // exit ASAP
+                if (srcDependsOnDst)
+                    return true;
+            }
+        }
+
+        return srcDependsOnDst;
+    }
+
+    public BlockDescriptor getBlock(int index)
+    {
+        return orderedBlocks.get(index);
+    }
+
+    public BlockDescriptor getBlockByID(int blockID) throws NoSuchBlockException
+    {
+        if (blockID == descriptor.getID())
+            return descriptor;
+
+        for (BlockDescriptor bd : orderedBlocks)
+            if (bd.getID() == blockID)
+                return bd;
+
+        throw new NoSuchBlockException(blockID, this);
+    }
+
+    /**
+     * @param variable
+     *        The input variable to retrieve.
+     * @return A unique ID for the variable within this work flow (used for XML loading/saving).
+     */
+    public String getInputVarID(Var<?> variable)
+    {
+        BlockDescriptor owner = getInputOwner(variable);
+
+        return owner.getID() + ":" + owner.getVarID(variable);
+    }
+
+    /**
+     * @param variable
+     *        The input variable to retrieve.
+     * @return A unique ID for the variable within this work flow (used for XML loading/saving).
+     */
+    public String getOutputVarID(Var<?> variable)
+    {
+        BlockDescriptor owner = getOutputOwner(variable);
+
+        return owner.getID() + ":" + owner.getVarID(variable);
+    }
+
+    public BlockDescriptor getBlockDescriptor()
+    {
+        return descriptor;
+    }
+
+    public BlockDescriptor getInputOwner(Var<?> var)
+    {
+        for (BlockDescriptor blockInfo : orderedBlocks)
+            for (Var<?> inputVar : blockInfo.inputVars)
+                if (inputVar.equals(var))
+                    return blockInfo;
+
+        return null;
+    }
+
+    public BlockDescriptor getOutputOwner(Var<?> var)
+    {
+        for (BlockDescriptor blockInfo : orderedBlocks)
+            for (Var<?> outputVar : blockInfo.outputVars)
+                if (outputVar.equals(var))
+                    return blockInfo;
+
+        return null;
+    }
+
+    public Iterable<Link<?>> getLinksIterator()
+    {
+        return new Iterable<Link<?>>()
+        {
+            @Override
+            public Iterator<Link<?>> iterator()
+            {
+                return links.iterator();
+            }
+        };
+    }
+
+    public int indexOf(BlockDescriptor blockInfo)
+    {
+        if (blockInfo == null)
+            return -1;
+
+        return orderedBlocks.indexOf(blockInfo);
+    }
+
+    @Override
+    public Iterator<BlockDescriptor> iterator()
+    {
+        // return orderedBlocks.iterator();
+        return new Iterator<BlockDescriptor>()
+        {
+            private final Iterator<BlockDescriptor> orderedBlocksIt = orderedBlocks.iterator();
+
+            @Override
+            public boolean hasNext()
+            {
+                return orderedBlocksIt.hasNext();
+            }
+
+            @Override
+            public BlockDescriptor next()
+            {
+                return orderedBlocksIt.next();
+            }
+
+            @Override
+            public void remove()
+            {
+                throw new UnsupportedOperationException();
+            }
+
+        };
+    }
+
+    /**
+     * Give the highest priority to the specified block, by placing giving it the smallest possible
+     * index in the list (i.e. after its direct dependencies). This method reorganizes the block execution order.
+     * 
+     * @param blockDesc
+     *        Descriptor of the block to be prioritized.
+     */
+    public void prioritize(BlockDescriptor blockDesc)
+    {
+        // retrieve the current order
+        int currentOrder = indexOf(blockDesc);
+        // assume we can give the block maximum priority
+        int targetOrder = 0;
+
+        // find its dependencies
+        HashSet<BlockDescriptor> dependencies = new HashSet<BlockDescriptor>();
+
+        for (Link<?> link : links)
+            if (link.dstBlock == blockDesc && contains(link.srcBlock))
+                dependencies.add(link.srcBlock);
+
+        // give piority to the dependencies first
+        for (BlockDescriptor dependency : dependencies)
+            prioritize(dependency);
+
+        // calculate the final order
+        for (BlockDescriptor dependency : dependencies)
+        {
+            int order = indexOf(dependency) + 1;
+            if (order > targetOrder)
+                targetOrder = order;
+        }
+
+        // assign the final order
+
+        if (targetOrder != currentOrder)
+        {
+            orderedBlocks.add(targetOrder, orderedBlocks.remove(currentOrder));
+            for (WorkFlowListener listener : listeners)
+                listener.workFlowReordered(this);
+        }
+    }
+
+    /**
+     * Re-orders the specified blocks to ensure that prevBlock will execute before nextBlock.<br>
+     * NOTE: this method assumes that the specified blocks belong to the current work flow. If this
+     * is not the case, see the {@link #reOrder(Link)} method instead.
+     * 
+     * @param prevBlock
+     *        The block that should be executed right before this instance.
+     * @param nextBlock
+     *        The block that should be executed right after this instance.
+     */
+    private void reOrder(BlockDescriptor prevBlock, BlockDescriptor nextBlock)
+    {
+        orderedBlocks.remove(prevBlock);
+        orderedBlocks.add(orderedBlocks.indexOf(nextBlock), prevBlock);
+
+        for (Link<?> link : links)
+            if (link.dstBlock == prevBlock && orderedBlocks.indexOf(link.srcBlock) > orderedBlocks.indexOf(prevBlock))
+            {
+                reOrder(link.srcBlock, prevBlock);
+                break; // only do it once, even if multiple links exist
+            }
+
+        for (WorkFlowListener listener : listeners)
+            listener.workFlowReordered(this);
+    }
+
+    public void removeBlock(BlockDescriptor blockInfo, boolean checkSelection)
+    {
+        // pops out workflows' selections
+        if (blockInfo.getBlock() instanceof WorkFlow)
+        {
+            WorkFlow wf = ((WorkFlow) blockInfo.getBlock());
+            if (checkSelection && !wf.getBlockSelection().isEmpty())
+                try
+                {
+                    // use location in main container instead of in the inner workflow
+                    // avoids jumping toward the top left
+                    for (BlockDescriptor bd : wf)
+                    {
+                        Point contLoc = bd.getContainer().getBlockDescriptor().getLocation();
+                        Point loc = bd.getLocation();
+                        bd.setLocation(contLoc.x + loc.x, contLoc.y + loc.y);
+                    }
+                    MainFrame.copySelection(wf, true);
+                    MainFrame.pasteSelection(this, true);
+                }
+                catch (LinkCutException e)
+                {
+                    if (ConfirmDialog.confirm("Warning", e.getMessage(), ConfirmDialog.OK_CANCEL_OPTION))
+                    {
+                        MainFrame.pasteSelection(this, true);
+                    }
+                    else
+                        return;
+                }
+            else
+            {
+                checkSelection = false;
+            }
+        }
+
+        // 1) remove all links to/from this block
+        // this must be done first for self-adjusting blocks
+        // example: Accumulator may remove some variables as soon as they get unlinked
+
+        // 1.a) links from this block elsewhere
+        for (int i = 0; i < links.size(); i++)
+        {
+            Link<?> link = links.get(i);
+
+            if (blockInfo == link.srcBlock)
+            {
+                link.dstVar.setReference(null);
+                links.remove(i--);
+
+                for (WorkFlowListener listener : listeners)
+                    listener.linkRemoved(this, link);
+            }
+        }
+
+        // 1.b) links from elsewhere to this block
+        for (int i = 0; i < links.size(); i++)
+        {
+            Link<?> link = links.get(i);
+
+            if (blockInfo == link.dstBlock)
+            {
+                link.dstVar.setReference(null);
+                links.remove(i--);
+
+                for (WorkFlowListener listener : listeners)
+                    listener.linkRemoved(this, link);
+            }
+        }
+
+        // 2) remove the block variables (this removes their exposure as well)
+
+        // a work flow does not have variables of its own => nothing to remove
+        if (!blockInfo.isWorkFlow())
+        {
+            // Remove input variables
+            for (Var<?> inputVar : blockInfo.inputVars)
+                this.descriptor.removeInput(inputVar);
+
+            // Remove output variables
+            for (Var<?> outputVar : blockInfo.outputVars)
+                this.descriptor.removeOutput(outputVar);
+        }
+
+        // 3) if the block is a work flow, remove its inner blocks
+
+        // if (blockInfo.isWorkFlow())
+        // {
+        // WorkFlow innerFlow = (WorkFlow) blockInfo.getBlock();
+        // while (innerFlow.size() > 0)
+        // innerFlow.removeBlock(innerFlow.getBlock(0), checkSelection);
+        // }
+
+        // 4) remove the block itself
+
+        orderedBlocks.remove(blockInfo);
+
+        blockInfo.removeBlockListener(this);
+
+        // 5) Notify listeners...
+
+        for (WorkFlowListener listener : listeners)
+        {
+            listener.blockRemoved(this, blockInfo);
+            listener.workFlowReordered(this);
+        }
+
+        blockInfo.inputVars.removeVarListListener(inputVarListener);
+        blockInfo.outputVars.removeVarListListener(outputVarListener);
+
+        blockSelection.remove(blockInfo);
+    }
+
+    /**
+     * @param var
+     *        the variable (input or output) to look for
+     * @return true if the specified variable is linked to another block, false otherwise
+     */
+    public boolean isLinked(Var<?> var)
+    {
+        for (Link<?> link : links)
+            if (link.dstVar == var || link.srcVar == var)
+                return true;
+        return false;
+    }
+
+    /**
+     * Removes the link to the specified variable, and returns false if this variable wasn't linked
+     * 
+     * @param dstVar
+     *        the destination variable of the link to remove
+     */
+    public void removeLink(Var<?> dstVar)
+    {
+        for (Link<?> link : links)
+            if (link.dstVar == dstVar && links.remove(link))
+            {
+                link.dstVar.setReference(null);
+
+                for (WorkFlowListener listener : listeners)
+                    listener.linkRemoved(this, link);
+
+                return;
+            }
+
+        // if code runs here, no link was found
+        // => check in the parent work flow for links to an "exposed" variable
+
+        if (descriptor.getContainer() != null)
+        {
+            descriptor.getContainer().removeLink(dstVar);
+        }
+        else
+        {
+            throw new NoSuchLinkException("In method WorkFlow.removeLink():\nNo link points to "
+                    + getInputOwner(dstVar).getName() + " > " + dstVar.getName());
+        }
+    }
+
+    public void removeListener(WorkFlowListener listener)
+    {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Resets all output variables of each block in this work flow to their default value
+     */
+    public void reset()
+    {
+        for (BlockDescriptor block : orderedBlocks)
+            block.reset();
+    }
+
+    /**
+     * Resets and runs this work flow in an independent thread.
+     */
+    public void runWorkFlow()
+    {
+        runWorkFlow(false);
+    }
+
+    /**
+     * Resets and runs this work flow in an independent thread.
+     * 
+     * @param waitForCompletion
+     *        <code>true</code> if this method should wait for the work flow to finish before
+     *        returning, or <code>false</code> if the method should return immediately (in this
+     *        case, the status of the work flow can be accessed via {@link #getDescriptor()}
+     *        {@link BlockDescriptor#getStatus() .getStatus()})
+     */
+    public void runWorkFlow(boolean waitForCompletion)
+    {
+        // this is the top-level work flow
+        // force it "dirty" to run it
+        this.descriptor.setStatus(BlockStatus.DIRTY);
+        executionThread = new Thread(this.descriptor, "Workflow");
+        executionThread.start();
+
+        if (waitForCompletion)
+        {
+            while (descriptor.getStatus() == BlockStatus.RUNNING)
+                ThreadUtil.sleep(100);
+        }
+    }
+
+    public void interrupt()
+    {
+        if (executionThread != null)
+        {
+            executionThread.interrupt();
+            userInterruption = true;
+        }
+        else
+        {
+            // this is not the top-level workflow
+            // => look for it
+            descriptor.getContainer().interrupt();
+        }
+    }
+
+    private boolean isInterrupted()
+    {
+        if (Icy.getMainInterface().isHeadLess())
+            return false;
+
+        if (descriptor.getContainer() != null)
+            return descriptor.getContainer().isInterrupted();
+
+        return userInterruption || executionThread.isInterrupted();
+    }
+
+    /**
+     * DO NOT CALL DIRECTLY FROM CLIENT CODE: this method assumes running in its own thread, and may
+     * behave inconsistently if interrupted. Use the {@link #runWorkFlow()} method instead
+     */
+    public void run()
+    {
+        if (orderedBlocks.size() == 0)
+            return;
+
+        descriptor.setStatus(BlockStatus.RUNNING);
+
+        BlockDescriptor runningBlock = null;
+
+        String finalStatus = "";
+        userInterruption = false;
+
+        long startTime = 0, endTime = 0;
+        try
+        {
+            startTime = System.nanoTime();
+
+            String statusPrefix = getBlockDescriptor().getContainer() == null ? ""
+                    : "\"" + getBlockDescriptor().getContainer().descriptor.getName() + "\"" + " => ";
+
+            // get the log stream recursively to ensure it is always the top-level one
+            PrintStream log = getLogStream();
+
+            for (int blockIndex = 0; blockIndex < orderedBlocks.size(); blockIndex++)
+            {
+                if (isInterrupted())
+                    throw new StopException();
+
+                BlockDescriptor blockDescriptor = orderedBlocks.get(blockIndex);
+
+                if (blockDescriptor.getStatus() != BlockStatus.READY)
+                {
+                    runningBlock = blockDescriptor;
+
+                    String blockLog = "block #" + (blockIndex + 1) + " [" + blockDescriptor.getDefinedName() + "]";
+
+                    // Adjust status
+                    statusChanged(this, "Running block " + statusPrefix + "\"" + blockDescriptor.getDefinedName()
+                            + "\" (#" + (blockIndex + 1) + ")...");
+
+                    // Log should be indented
+                    String indentation = "";
+                    // indent as many times as we go deep in the tree
+                    WorkFlow container = this.descriptor.getContainer();
+                    while (container != null)
+                    {
+                        indentation += "   ";
+                        container = container.descriptor.getContainer();
+                    }
+                    log.print(indentation + "Running " + blockLog);
+
+                    if (blockDescriptor.inputVars.size() > 0)
+                    {
+                        log.println(" with the following parameters:");
+                        for (Var<?> var : blockDescriptor.inputVars)
+                        {
+                            if (!blockDescriptor.inputVars.isVisible(var))
+                                continue;
+
+                            log.print(indentation + "- " + var.getName() + ": " + var.getValueAsString(true));
+
+                            // If this variable points to another one, say so
+                            Var<?> reference = var.getReference();
+                            if (reference != null)
+                            {
+                                // The owner of the reference is either...
+
+                                // 1) an output variable...
+                                BlockDescriptor owner = getOutputOwner(reference);
+                                // 2) a pass-through input...
+                                if (owner == null)
+                                    owner = getInputOwner(reference);
+                                // 3) a variable outside the work flow
+                                if (owner == null)
+                                {
+                                    log.println(" (from variable \"" + reference.getName()
+                                            + "\" defined outside this workflow)");
+                                }
+                                else if (owner == this.descriptor)
+                                {
+                                    log.println(" (from local loop or batch variable \"" + reference.getName() + "\")");
+                                }
+                                else
+                                {
+                                    log.println(" (from variable \"" + reference.getName() + "\" of block #"
+                                            + (indexOf(owner) + 1) + " [" + owner.getDefinedName() + "])");
+                                }
+                            }
+                            else
+                                log.println();
+                        }
+                    }
+
+                    // THIS IS WHERE IT HAPPENS
+                    long tic = System.nanoTime();
+                    blockDescriptor.run();
+                    long tac = System.nanoTime();
+
+                    String time = UnitUtil.displayTimeAsStringWithUnits((tac - tic) / 1000000, false);
+                    log.println(indentation + "Finished " + blockLog + " in " + (time.isEmpty() ? "0 ms" : time));
+
+                    runningBlock = null;
+
+                    if (blockDescriptor.isFinalBlock())
+                    {
+                        blockDescriptor.setFinalBlock(false);
+                        interrupt();
+                    }
+                }
+            }
+
+            if (descriptor.getContainer() != null)
+                log.println();
+
+            finalStatus = "The workflow executed successfully";
+        }
+        catch (StopException e)
+        {
+            // push the exception upstairs
+            if (descriptor.getContainer() != null)
+                throw e;
+
+            finalStatus = "The workflow was interrupted";
+        }
+        catch (RuntimeException e)
+        {
+            finalStatus = "The workflow did not execute properly";
+
+            boolean catchException = false;
+
+            if (e instanceof IcyHandledException || e instanceof VarException)
+            {
+                catchException = true;
+            }
+            else if (e instanceof BlocksException)
+            {
+                catchException = ((BlocksException) e).catchException;
+            }
+
+            if (catchException)
+            {
+                String blockName = runningBlock.getDefinedName();
+                int blockID = indexOf(runningBlock) + 1;
+                throw new IcyHandledException(
+                        "While running block \"" + blockName + "\" (" + blockID + "):\n" + e.getMessage());
+            }
+
+            // it's probably a real problem, re-throw
+            throw e;
+        }
+        finally
+        {
+            descriptor.setStatus(BlockStatus.READY);
+            endTime = System.nanoTime();
+            double time = endTime - startTime;
+            finalStatus += " (total running time: " + StringUtil.toString(time / 1e9, 2) + " seconds)";
+            statusChanged(this, finalStatus);
+        }
+    }
+
+    public void setLocation(BlockDescriptor blockInfo, Point point)
+    {
+        blockInfo.setLocation(point.x, point.y);
+    }
+
+    /**
+     * @return the number of blocks in this work flow
+     */
+    public int size()
+    {
+        return orderedBlocks.size();
+    }
+
+    @Override
+    public void blockStatusChanged(BlockDescriptor blockInfo, BlockStatus status)
+    {
+        if (status == BlockStatus.DIRTY)
+        {
+            // "dirty-fy" following blocks
+            for (Link<?> link : links)
+                if (link.srcBlock == blockInfo)
+                    link.dstBlock.setStatus(BlockStatus.DIRTY);
+        }
+    }
+
+    @Override
+    public void blockVariableAdded(BlockDescriptor block, Var<?> variable)
+    {
+
+    }
+
+    @Override
+    public <T> void blockVariableChanged(BlockDescriptor block, Var<T> variable, T newValue)
+    {
+        blockVariableChanged(this, block, variable, newValue);
+    }
+
+    @Override
+    public void blockCollapsed(BlockDescriptor block, boolean collapsed)
+    {
+
+    }
+
+    @Override
+    public void blockDimensionChanged(BlockDescriptor block, int newWidth, int newHeight)
+    {
+        blockDimensionChanged(this, block, newWidth, newHeight);
+    }
+
+    @Override
+    public void blockLocationChanged(BlockDescriptor block, int newX, int newY)
+    {
+        blockLocationChanged(this, block, newX, newY);
+    }
+
+    @Override
+    public void blockAdded(WorkFlow source, BlockDescriptor addedBlock)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.blockAdded(source, addedBlock);
+    }
+
+    @Override
+    public void blockRemoved(WorkFlow source, BlockDescriptor removedBlock)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.blockRemoved(source, removedBlock);
+    }
+
+    @Override
+    public void linkAdded(WorkFlow source, Link<?> addedLink)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.linkAdded(source, addedLink);
+    }
+
+    @Override
+    public void linkRemoved(WorkFlow source, Link<?> removedLink)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.linkRemoved(source, removedLink);
+    }
+
+    @Override
+    public void workFlowReordered(WorkFlow source)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.workFlowReordered(source);
+    }
+
+    @Override
+    public void blockCollapsed(WorkFlow source, BlockDescriptor block, boolean collapsed)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.blockCollapsed(source, block, collapsed);
+    }
+
+    @Override
+    public void blockDimensionChanged(WorkFlow source, BlockDescriptor block, int newWidth, int newHeight)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.blockDimensionChanged(this, block, newWidth, newHeight);
+    }
+
+    @Override
+    public void blockLocationChanged(WorkFlow source, BlockDescriptor block, int newX, int newY)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.blockLocationChanged(this, block, newX, newY);
+    }
+
+    @Override
+    public void blockStatusChanged(WorkFlow source, BlockDescriptor block, BlockStatus status)
+    {
+        // no need to propagate this event
+    }
+
+    @Override
+    public void blockVariableAdded(WorkFlow source, BlockDescriptor block, Var<?> variable)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.blockVariableAdded(this, block, variable);
+    }
+
+    @Override
+    public <T> void blockVariableChanged(WorkFlow source, BlockDescriptor block, Var<T> variable, T newValue)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.blockVariableChanged(this, block, variable, newValue);
+    }
+
+    @Override
+    public void statusChanged(WorkFlow source, String message)
+    {
+        for (WorkFlowListener listener : listeners)
+            listener.statusChanged(source, message);
+    }
+
+    public void newSelection()
+    {
+        blockSelection = new ArrayList<BlockDescriptor>();
+        linkSelection = new ArrayList<Link<?>>();
+    }
+
+    public boolean isBlockSelected(BlockDescriptor bd)
+    {
+        return blockSelection.contains(bd);
+    }
+
+    public boolean isLinkSelected(Link<?> l)
+    {
+        return linkSelection.contains(l);
+    }
+
+    public void selectBlock(BlockDescriptor bd)
+    {
+        if (!isBlockSelected(bd))
+            blockSelection.add(bd);
+    }
+
+    public void selectLink(Link<?> l)
+    {
+        if (!isLinkSelected(l))
+            linkSelection.add(l);
+    }
+
+    public void unselectBlock(BlockDescriptor bd)
+    {
+        if (isBlockSelected(bd))
+            blockSelection.remove(bd);
+    }
+
+    public void unselectLink(Link<?> l)
+    {
+        if (isLinkSelected(l))
+            linkSelection.remove(l);
+    }
+
+    public ArrayList<BlockDescriptor> getBlockSelection()
+    {
+        return blockSelection;
+    }
+
+    public ArrayList<Link<?>> getLinkSelection()
+    {
+        return linkSelection;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/Accumulator.java b/src/main/java/plugins/adufour/blocks/tools/Accumulator.java
new file mode 100644
index 0000000000000000000000000000000000000000..01809e554f02afef889de48b98922f2800968cec
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/Accumulator.java
@@ -0,0 +1,142 @@
+package plugins.adufour.blocks.tools;
+
+import java.lang.reflect.Array;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.blocks.util.VarListListener;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarTrigger;
+import plugins.adufour.vars.util.TypeChangeListener;
+import plugins.adufour.vars.util.VarReferencingPolicy;
+
+public class Accumulator extends Plugin implements ToolsBlock, TypeChangeListener
+{
+    Class<?>   type   = null;
+    
+    VarList    inputMap;
+    
+    VarMutable output = new VarMutable("output", null);
+    
+    @Override
+    public void run()
+    {
+        int arrayLength = 0;
+        
+        for (Var<?> var : inputMap)
+        {
+            if (var.getValue() == null || var instanceof VarTrigger) continue;
+            
+            if (var.getValue().getClass().isArray())
+            {
+                arrayLength += Array.getLength(var.getValue());
+            }
+            else arrayLength++;
+        }
+        
+        Object array = Array.newInstance(type.getComponentType(), arrayLength);
+        
+        int i = 0;
+        for (Var<?> var : inputMap)
+        {
+            if (var.getValue() == null || var instanceof VarTrigger) continue;
+            
+            if (var.getValue().getClass().isArray())
+            {
+                int length = Array.getLength(var.getValue());
+                System.arraycopy(var.getValue(), 0, array, i, length);
+                i += length;
+            }
+            else Array.set(array, i++, var.getValue());
+        }
+        
+        output.setValue(array);
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Override
+    public void declareInput(final VarList theInputMap)
+    {
+        if (this.inputMap == null)
+        {
+            this.inputMap = theInputMap;
+            VarTrigger trigger = new VarTrigger("Add variable", new VarTrigger.TriggerListener()
+            {
+                @Override
+                public void valueChanged(Var<Integer> source, Integer oldValue, Integer newValue)
+                {
+                    
+                }
+                
+                @Override
+                public void referenceChanged(Var<Integer> source, Var<? extends Integer> oldReference, Var<? extends Integer> newReference)
+                {
+                    
+                }
+                
+                @Override
+                public void triggered(VarTrigger source)
+                {
+                    final VarMutable var = new VarMutable("input", type)
+                    {
+                        public boolean isAssignableFrom(Var aVariable)
+                        {
+                            if (this.type == null) return true;
+                            
+                            if (super.isAssignableFrom(aVariable)) return true;
+                            
+                            return type.getComponentType().isAssignableFrom(aVariable.getType());
+                        };
+                    };
+                    var.addTypeChangeListener(Accumulator.this);
+                    theInputMap.addRuntimeVariable("" + var.hashCode(), var);
+                }
+            });
+            
+            trigger.setReferencingPolicy(VarReferencingPolicy.NONE);
+            
+            theInputMap.add("Add variable", trigger);
+            
+            theInputMap.addVarListListener(new VarListListener()
+            {
+                @Override
+                public void variableRemoved(VarList list, Var variable)
+                {
+                }
+                
+                @Override
+                public void variableAdded(VarList list, Var variable)
+                {
+                    if (variable instanceof VarMutable)
+                    {
+                        ((VarMutable) variable).addTypeChangeListener(Accumulator.this);
+                    }
+                }
+            });
+        }
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output", output);
+    }
+    
+    @Override
+    public void typeChanged(Object source, Class<?> oldType, Class<?> newType)
+    {
+        if (type == null)
+        {
+            this.type = (newType.isArray() ? newType : Array.newInstance(newType, 0).getClass());
+            
+            output.setType(type);
+            
+            for (Var<?> var : inputMap)
+            {
+                if (var instanceof VarMutable) ((VarMutable) var).setType(type);
+            }
+        }
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/Display.java b/src/main/java/plugins/adufour/blocks/tools/Display.java
new file mode 100644
index 0000000000000000000000000000000000000000..f77cbd1905347645f0e92022196325d86d7e39ab
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/Display.java
@@ -0,0 +1,52 @@
+package plugins.adufour.blocks.tools;
+
+import icy.main.Icy;
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.lang.Block;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.util.VarListener;
+
+public class Display extends Plugin implements Block
+{
+    private VarMutable object = new VarMutable("object", null);
+    
+    @Override
+    public void run()
+    {
+        if (Icy.getMainInterface().isHeadLess())
+        {
+            // Print to the standard output stream
+            System.out.println(object.getValueAsString());
+        }
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("object", object);
+        
+        object.addListener(new VarListener<Object>()
+        {
+            @Override
+            public void referenceChanged(Var<Object> source, Var<? extends Object> oldReference, Var<? extends Object> newReference)
+            {
+                if (newReference == null && !object.isReferenced()) object.setType(null);
+            }
+            
+            @Override
+            public void valueChanged(Var<Object> source, Object oldValue, Object newValue)
+            {
+                
+            }
+        });
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/Indexer.java b/src/main/java/plugins/adufour/blocks/tools/Indexer.java
new file mode 100644
index 0000000000000000000000000000000000000000..c88d8d23373654d607702cfbdffe109952c980fb
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/Indexer.java
@@ -0,0 +1,61 @@
+package plugins.adufour.blocks.tools;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarInteger;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarMutableArray;
+import plugins.adufour.vars.util.VarException;
+import plugins.adufour.vars.util.VarListener;
+
+/**
+ * Utility block reading the specified index of an input array. This block uses mutable types to
+ * receive any type of input and adjust the output accordingly
+ * 
+ * @author Alexandre Dufour
+ */
+public class Indexer extends Plugin implements ToolsBlock
+{
+    VarMutableArray array   = new VarMutableArray("array", null);
+    VarInteger      index   = new VarInteger("index", 0);
+    
+    VarMutable      element = new VarMutable("element", null);
+    
+    @Override
+    public void run()
+    {
+        if (index.getValue() >= array.size()) throw new VarException(index, "Index " + index.getValueAsString() + " does not exist (array size: " + array.size() + ")");
+        
+        element.setValue(array.getElementAt(index.getValue()));
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("array", array);
+        inputMap.add("index", index);
+        
+        array.addListener(new VarListener()
+        {
+            @Override
+            public void valueChanged(Var source, Object oldValue, Object newValue)
+            {
+            }
+            
+            @Override
+            public void referenceChanged(Var source, Var oldReference, Var newReference)
+            {
+                element.setType(newReference == null ? null : newReference.getType().getComponentType());
+            }
+        });
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("element", element);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/Iterator.java b/src/main/java/plugins/adufour/blocks/tools/Iterator.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e0c05f70cc134cc1eb1bdeb3c6e284288d17a5c
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/Iterator.java
@@ -0,0 +1,68 @@
+package plugins.adufour.blocks.tools;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarBoolean;
+import plugins.adufour.vars.lang.VarInteger;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarMutableArray;
+import plugins.adufour.vars.util.VarListener;
+
+/**
+ * Utility block reading the specified index of an input array. This block uses mutable types to
+ * receive any type of input and adjust the output accordingly
+ * 
+ * @author Alexandre Dufour
+ */
+public class Iterator extends Plugin implements ToolsBlock
+{
+    VarMutableArray array   = new VarMutableArray("array", null);
+    VarInteger      index   = new VarInteger("index", 0);
+    VarBoolean      end     = new VarBoolean("end", false);
+    VarMutable      element = new VarMutable("element", null);
+    
+    @Override
+    public void run()
+    {
+        if (array.getValue() != null && index.getValue() < array.size())
+        {
+            element.setValue(array.getElementAt(index.getValue()));
+            index.setValue(index.getValue() + 1);
+        }
+        else
+        {
+            end.setValue(true);
+        }
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("array", array);
+        
+        array.addListener(new VarListener()
+        {
+            @Override
+            public void valueChanged(Var source, Object oldValue, Object newValue)
+            {
+            }
+            
+            @Override
+            public void referenceChanged(Var source, Var oldReference, Var newReference)
+            {
+                element.setType(newReference == null ? null : newReference.getType().getComponentType());
+            }
+        });
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("element", element);
+        outputMap.add("index", index);
+        outputMap.add("end", end);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ListSize.java b/src/main/java/plugins/adufour/blocks/tools/ListSize.java
new file mode 100644
index 0000000000000000000000000000000000000000..998273cb8041a7c12e6d37b65f22b425292a5f21
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ListSize.java
@@ -0,0 +1,31 @@
+package plugins.adufour.blocks.tools;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarInteger;
+import plugins.adufour.vars.lang.VarMutableArray;
+
+public class ListSize extends Plugin implements ToolsBlock
+{
+    private final VarMutableArray list = new VarMutableArray("List", null);
+    
+    private final VarInteger      size = new VarInteger("Size", 0);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("list", list);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("size", size);
+    }
+    
+    @Override
+    public void run()
+    {
+        size.setValue(list.size());
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ReLoop.java b/src/main/java/plugins/adufour/blocks/tools/ReLoop.java
new file mode 100644
index 0000000000000000000000000000000000000000..f8bbee02170503da973a7694ae2009e8e2d4260f
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ReLoop.java
@@ -0,0 +1,68 @@
+package plugins.adufour.blocks.tools;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.util.TypeChangeListener;
+
+public class ReLoop extends Plugin implements ToolsBlock
+{
+    public final VarMutable initValue   = new VarMutable("init", null);
+    
+    public final VarMutable reloopValue = new VarMutable("reloop", null);
+    
+    public final VarMutable output      = new VarMutable("output", null);
+    
+    @Override
+    public void run()
+    {
+        // first run: output is null
+        if (output.getValue() == null)
+        {
+            output.setValue(initValue.getValue());
+        }
+        else
+        {
+            output.setValue(reloopValue.getValue());
+        }
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("init", initValue);
+        inputMap.add("reloop", reloopValue);
+        
+        initValue.addTypeChangeListener(new TypeChangeListener()
+        {
+            @Override
+            public void typeChanged(Object source, Class<?> oldType, Class<?> newType)
+            {
+                if (newType != null)
+                {
+                    reloopValue.setType(newType);
+                    output.setType(newType);
+                }
+            }
+        });
+        
+        reloopValue.addTypeChangeListener(new TypeChangeListener()
+        {
+            @Override
+            public void typeChanged(Object source, Class<?> oldType, Class<?> newType)
+            {
+                if (newType != null)
+                {
+                    initValue.setType(newType);
+                    output.setType(newType);
+                }
+            }
+        });
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output", output);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ToolsBlock.java b/src/main/java/plugins/adufour/blocks/tools/ToolsBlock.java
new file mode 100644
index 0000000000000000000000000000000000000000..f447b9668961cbafe013e0e2c20fbb4d9d97a608
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ToolsBlock.java
@@ -0,0 +1,8 @@
+package plugins.adufour.blocks.tools;
+
+import plugins.adufour.blocks.lang.Block;
+
+public interface ToolsBlock extends Block
+{
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ij/CallIJMacro.java b/src/main/java/plugins/adufour/blocks/tools/ij/CallIJMacro.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc5262f002af8cd4c9c4c12a2c534f8cfabe7ac4
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ij/CallIJMacro.java
@@ -0,0 +1,30 @@
+package plugins.adufour.blocks.tools.ij;
+
+import icy.plugin.abstract_.Plugin;
+import ij.IJ;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarFile;
+
+public class CallIJMacro extends Plugin implements IJBlock
+{
+    VarFile macroFile = new VarFile("Macro file", null);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("Macro file", macroFile);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        
+    }
+    
+    @Override
+    public void run()
+    {
+        IJ.runMacroFile(macroFile.getValue(true).getPath());
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ij/CallIJPlugin.java b/src/main/java/plugins/adufour/blocks/tools/ij/CallIJPlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..31330e0d4947427dde46235f072f4e67024726ec
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ij/CallIJPlugin.java
@@ -0,0 +1,61 @@
+package plugins.adufour.blocks.tools.ij;
+
+import icy.plugin.abstract_.Plugin;
+import icy.system.IcyHandledException;
+import ij.IJ;
+import ij.ImagePlus;
+import ij.WindowManager;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarImagePlus;
+import plugins.adufour.vars.lang.VarString;
+
+public class CallIJPlugin extends Plugin implements IJBlock
+{
+    VarImagePlus varIp = new VarImagePlus("Input ImagePlus", null);
+    
+    VarString pluginName = new VarString("plug-in name", "");
+    
+    VarString pluginParams = new VarString("parameters", "");
+    
+    VarImagePlus varActiveIP = new VarImagePlus("Output (active) ImagePlus", null);
+    
+    @Override
+    public void run()
+    {
+        try
+        {
+            IJ.run(varIp.getValue(true), pluginName.getValue(true), pluginParams.getValue(true));
+            // Set the output image (if available) with the following priority
+            ImagePlus output = WindowManager.getCurrentImage();
+            if (output == null)
+            {
+                // Default to the current "temporary" image (if any)
+                output = WindowManager.getTempCurrentImage();
+            }
+            if (output == null)
+            {
+                // Default to the input image (may have been modified "in-place"
+                output = varIp.getValue();
+            }
+            varActiveIP.setValue(output);
+        }
+        catch (RuntimeException e)
+        {
+            throw new IcyHandledException(e.getLocalizedMessage());
+        }
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("Input ImagePlus", varIp);
+        inputMap.add("ImageJ plug-in name", pluginName);
+        inputMap.add("ImageJ plug-in parameters", pluginParams);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("Output ImagePlus", varActiveIP);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ij/IJBlock.java b/src/main/java/plugins/adufour/blocks/tools/ij/IJBlock.java
new file mode 100644
index 0000000000000000000000000000000000000000..39c20dc2b46738302f7ae75f5c5dc444a59d8791
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ij/IJBlock.java
@@ -0,0 +1,13 @@
+package plugins.adufour.blocks.tools.ij;
+
+import plugins.adufour.blocks.lang.Block;
+
+/**
+ * Interface used to flag ImageJ-related blocks
+ * 
+ * @author Alexandre Dufour
+ * 
+ */
+public interface IJBlock extends Block
+{
+}
\ No newline at end of file
diff --git a/src/main/java/plugins/adufour/blocks/tools/ij/ImagePlusToSequence.java b/src/main/java/plugins/adufour/blocks/tools/ij/ImagePlusToSequence.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef666887f5d9772d037e8b5d345dbc60511a3ce0
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ij/ImagePlusToSequence.java
@@ -0,0 +1,32 @@
+package plugins.adufour.blocks.tools.ij;
+
+import icy.imagej.ImageJUtil;
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarImagePlus;
+import plugins.adufour.vars.lang.VarSequence;
+
+public class ImagePlusToSequence extends Plugin implements IJBlock
+{
+    VarImagePlus vip = new VarImagePlus("IJ ImagePlus", null);
+    VarSequence  vs  = new VarSequence("Icy Sequence", null);
+    
+    @Override
+    public void run()
+    {
+        vs.setValue(ImageJUtil.convertToIcySequence(vip.getValue(true), null));
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("IJ ImagePlus", vip);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("Icy Sequence", vs);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ij/SequenceToImagePlus.java b/src/main/java/plugins/adufour/blocks/tools/ij/SequenceToImagePlus.java
new file mode 100644
index 0000000000000000000000000000000000000000..57e5ff055c95f5a45809c64c338f36b0e1b479f5
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ij/SequenceToImagePlus.java
@@ -0,0 +1,32 @@
+package plugins.adufour.blocks.tools.ij;
+
+import icy.imagej.ImageJUtil;
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarImagePlus;
+import plugins.adufour.vars.lang.VarSequence;
+
+public class SequenceToImagePlus extends Plugin implements IJBlock
+{
+    VarSequence  vs  = new VarSequence("Icy Sequence", null);
+    VarImagePlus vip = new VarImagePlus("ImagePlus", null);
+    
+    @Override
+    public void run()
+    {
+        vip.setValue(ImageJUtil.convertToImageJImage(vs.getValue(true), null));
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("Icy Sequence", vs);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("IJ ImagePlus", vip);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/ij/ShowImagePlus.java b/src/main/java/plugins/adufour/blocks/tools/ij/ShowImagePlus.java
new file mode 100644
index 0000000000000000000000000000000000000000..f18b07ccccfebfb28122f6a7739164386b89d900
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/ij/ShowImagePlus.java
@@ -0,0 +1,28 @@
+package plugins.adufour.blocks.tools.ij;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarImagePlus;
+
+public class ShowImagePlus extends Plugin implements IJBlock
+{
+    VarImagePlus ip = new VarImagePlus("ImagePlus", null);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("ImagePlus", ip);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+    @Override
+    public void run()
+    {
+        ip.getValue(true).show();
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Boolean.java b/src/main/java/plugins/adufour/blocks/tools/input/Boolean.java
new file mode 100644
index 0000000000000000000000000000000000000000..4eff08bb4d172c2c330c67329e5800fbd0c7367c
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Boolean.java
@@ -0,0 +1,24 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarBoolean;
+
+public class Boolean extends Plugin implements InputBlock
+{
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("boolean", new VarBoolean("Boolean", false));
+    }
+
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+
+    @Override
+    public void run()
+    {
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Decimal.java b/src/main/java/plugins/adufour/blocks/tools/input/Decimal.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e345aa3c7ea59f3008e0e96d83465daab2470f6
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Decimal.java
@@ -0,0 +1,29 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarDouble;
+
+/**
+ * Input block reading a 64-bit double-precision floating-point value
+ * 
+ * @author Alexandre Dufour
+ */
+public class Decimal extends Plugin implements InputBlock
+{
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("decimal", new VarDouble("decimal", 0.0));
+    }
+    
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Decimals.java b/src/main/java/plugins/adufour/blocks/tools/input/Decimals.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ab76db439db35edf19dc9adc6fb2422bbbc7e5b
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Decimals.java
@@ -0,0 +1,29 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarDoubleArrayNative;
+
+/**
+ * Utility block reading an array of double-precision floating-point values
+ * 
+ * @author Alexandre Dufour
+ */
+public class Decimals extends Plugin implements InputBlock
+{
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("decimals", new VarDoubleArrayNative("decimals", new double[0]));
+    }
+    
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/File.java b/src/main/java/plugins/adufour/blocks/tools/input/File.java
new file mode 100644
index 0000000000000000000000000000000000000000..141604ad6a15e3a84453fd05f95da66c7178817b
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/File.java
@@ -0,0 +1,34 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeModel;
+import plugins.adufour.vars.lang.VarFile;
+
+/**
+ * Input block reading a file
+ * 
+ * @author Alexandre Dufour
+ */
+public class File extends Plugin implements InputBlock
+{
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        VarFile vf = new VarFile("file", null);
+        vf.setDefaultEditorModel(new FileTypeModel("", FileMode.FILES, null, false));
+        inputMap.add("file", vf);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Files.java b/src/main/java/plugins/adufour/blocks/tools/input/Files.java
new file mode 100644
index 0000000000000000000000000000000000000000..38592b4887ca4084a6342390127b2f9086af4cbd
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Files.java
@@ -0,0 +1,34 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeListModel;
+import plugins.adufour.vars.lang.VarFileArray;
+
+/**
+ * Input block reading a list of files
+ * 
+ * @author Alexandre Dufour
+ */
+public class Files extends Plugin implements InputBlock
+{
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        VarFileArray vf = new VarFileArray("files", new java.io.File[0]);
+        vf.setDefaultEditorModel(new FileTypeListModel("", FileMode.FILES, null, false));
+        inputMap.add("files", vf);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Folder.java b/src/main/java/plugins/adufour/blocks/tools/input/Folder.java
new file mode 100644
index 0000000000000000000000000000000000000000..af38bad05eaa482221b9af005ad95681e32d164f
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Folder.java
@@ -0,0 +1,34 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeModel;
+import plugins.adufour.vars.lang.VarFile;
+
+/**
+ * Input block reading a folder
+ * 
+ * @author Alexandre Dufour
+ */
+public class Folder extends Plugin implements InputBlock
+{
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        VarFile vf = new VarFile("folder", null);
+        vf.setDefaultEditorModel(new FileTypeModel("", FileMode.FOLDERS, null, false));
+        inputMap.add("folder", vf);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Folders.java b/src/main/java/plugins/adufour/blocks/tools/input/Folders.java
new file mode 100644
index 0000000000000000000000000000000000000000..038676e660269736be7b35e0850af4e273b798d3
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Folders.java
@@ -0,0 +1,34 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeListModel;
+import plugins.adufour.vars.lang.VarFileArray;
+
+/**
+ * Input block reading a list of folders
+ * 
+ * @author Alexandre Dufour
+ */
+public class Folders extends Plugin implements InputBlock
+{
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        VarFileArray vf = new VarFileArray("folders", new java.io.File[0]);
+        vf.setDefaultEditorModel(new FileTypeListModel("", FileMode.FOLDERS, null, false));
+        inputMap.add("folders", vf);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/InputBlock.java b/src/main/java/plugins/adufour/blocks/tools/input/InputBlock.java
new file mode 100644
index 0000000000000000000000000000000000000000..a43653ea33d803bcd89d52870e6e347a23dfd96a
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/InputBlock.java
@@ -0,0 +1,13 @@
+package plugins.adufour.blocks.tools.input;
+
+import plugins.adufour.blocks.lang.Block;
+
+/**
+ * Interface used to flag pure input blocks
+ * 
+ * @author Alexandre Dufour
+ * 
+ */
+public interface InputBlock extends Block
+{
+}
\ No newline at end of file
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Integer.java b/src/main/java/plugins/adufour/blocks/tools/input/Integer.java
new file mode 100644
index 0000000000000000000000000000000000000000..514ca1ed9253df68ee021265a544052e55323aa2
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Integer.java
@@ -0,0 +1,30 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarInteger;
+
+/**
+ * Utility block reading a 32-bit integer value
+ * 
+ * @author Alexandre Dufour
+ */
+public class Integer extends Plugin implements InputBlock
+{
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        VarInteger vi = new VarInteger("integer", 0);
+        inputMap.add("integer", vi);
+    }
+    
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Integers.java b/src/main/java/plugins/adufour/blocks/tools/input/Integers.java
new file mode 100644
index 0000000000000000000000000000000000000000..acae7c2034e09fb0076eb22e26147b7f7a9d8868
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Integers.java
@@ -0,0 +1,29 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarIntegerArrayNative;
+
+/**
+ * Utility block reading an array of 32-bit integer values
+ * 
+ * @author Alexandre Dufour
+ */
+public class Integers extends Plugin implements InputBlock
+{
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("integers", new VarIntegerArrayNative("integers", new int[0]));
+    }
+    
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Sequence.java b/src/main/java/plugins/adufour/blocks/tools/input/Sequence.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd2ab2042e8ee1065c3578dd49aee0f1914089d4
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Sequence.java
@@ -0,0 +1,36 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarSequence;
+
+/**
+ * Utility block reading a {@link icy.sequence.Sequence} object
+ * 
+ * @author Alexandre Dufour
+ */
+public class Sequence extends Plugin implements InputBlock
+{
+    private final VarSequence vs = new VarSequence("sequence", null);
+    
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input sequence", vs);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+    public VarSequence getVariable()
+    {
+        return vs;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Sequences.java b/src/main/java/plugins/adufour/blocks/tools/input/Sequences.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf5d484013abc859b703438c329098f92517eadc
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Sequences.java
@@ -0,0 +1,30 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarSequenceArray;
+
+/**
+ * Utility block reading a list of {@link icy.sequence.Sequence} objects
+ * 
+ * @author Alexandre Dufour
+ */
+public class Sequences extends Plugin implements InputBlock
+{
+    @Override
+    public void run()
+    {
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        VarSequenceArray vsa = new VarSequenceArray("sequence");
+        inputMap.add("input sequence", vsa);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/input/Text.java b/src/main/java/plugins/adufour/blocks/tools/input/Text.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ced613314bdd60c89a2a09eef6d0db094b8f657
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/input/Text.java
@@ -0,0 +1,31 @@
+package plugins.adufour.blocks.tools.input;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarString;
+
+/**
+ * Text input
+ * 
+ * @author Alexandre Dufour
+ */
+public class Text extends Plugin implements InputBlock
+{
+    @Override
+    public void run()
+    {
+        
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("text", new VarString("text", ""));
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/AppendFilePath.java b/src/main/java/plugins/adufour/blocks/tools/io/AppendFilePath.java
new file mode 100644
index 0000000000000000000000000000000000000000..9cb77e369bb5c22ede5bce37336f16e2bec1462a
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/AppendFilePath.java
@@ -0,0 +1,58 @@
+package plugins.adufour.blocks.tools.io;
+
+import java.io.File;
+
+import icy.file.FileUtil;
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarBoolean;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarString;
+
+public class AppendFilePath extends Plugin implements IOBlock
+{
+    VarMutable    in        = new VarMutable("Current file", null);
+    
+    VarFile    out       = new VarFile("New file", null);
+    
+    VarBoolean removeExt = new VarBoolean("Remove extension", false);
+    
+    VarString  suffix    = new VarString("Add suffix", "");
+    
+    @Override
+    public void run()
+    {
+        Object input = in.getValue(true);
+        String newPath = "";
+        
+        if (input instanceof File)
+            newPath = ((File)input).getAbsolutePath();
+        else newPath = input.toString();
+        
+        if (removeExt.getValue())
+        {
+            String fileName = FileUtil.getFileName(newPath, false);
+            newPath = FileUtil.getDirectory(newPath) + fileName;
+        }
+        
+        newPath += suffix.getValue();
+        
+        out.setValue(new File(newPath));
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input file", in);
+        inputMap.add("remove ext.", removeExt);
+        inputMap.add("suffix", suffix);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output file", out);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/CreateFile.java b/src/main/java/plugins/adufour/blocks/tools/io/CreateFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3277826bdeeae9596f4940347a629884acab685
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/CreateFile.java
@@ -0,0 +1,62 @@
+package plugins.adufour.blocks.tools.io;
+
+import java.io.File;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeModel;
+import plugins.adufour.vars.lang.VarBoolean;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarString;
+import plugins.adufour.vars.util.VarException;
+import icy.file.FileUtil;
+import icy.plugin.abstract_.Plugin;
+
+public class CreateFile extends Plugin implements IOBlock
+{
+    VarFile parentFolder = new VarFile("Base folder", new File(System.getProperty("user.home")));
+    
+    VarString fileName = new VarString("File name", "newFile");
+    
+    VarFile file = new VarFile("New file", null);
+    
+    VarBoolean overwrite = new VarBoolean("Overwrite", false);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        parentFolder.setDefaultEditorModel(new FileTypeModel(parentFolder.getValue().getPath(), FileMode.FOLDERS, null, true));
+        inputMap.add("parent folder", parentFolder);
+        inputMap.add("file name", fileName);
+        inputMap.add("overwrite", overwrite);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("new file", file);
+    }
+    
+    @Override
+    public void run()
+    {
+        File parent = parentFolder.getValue();
+        
+        if (parent == null || !parent.exists() || !parent.isDirectory()) throw new VarException(parentFolder, "The parent folder does not exist or is not a folder");
+        
+        if (fileName.getValue().isEmpty()) throw new VarException(fileName, "The name of the file cannot be empty");
+        
+        File newFile = new File(parentFolder + File.separator + fileName.getValue());
+        
+        if (!newFile.exists())
+        {
+            FileUtil.createFile(newFile);
+        }
+        else if (!newFile.isDirectory() && !overwrite.getValue())
+        {
+            throw new VarException(fileName, "Cannot create file " + newFile.getPath() + "\n=> A file with this name already exists");
+        }
+        
+        file.setValue(newFile);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/CreateFolder.java b/src/main/java/plugins/adufour/blocks/tools/io/CreateFolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0efdc076a7a9bd3a418be64f90285ba35cb67cc
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/CreateFolder.java
@@ -0,0 +1,57 @@
+package plugins.adufour.blocks.tools.io;
+
+import icy.file.FileUtil;
+import icy.plugin.abstract_.Plugin;
+
+import java.io.File;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeModel;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarString;
+import plugins.adufour.vars.util.VarException;
+
+public class CreateFolder extends Plugin implements IOBlock
+{
+    VarFile   parentFolder = new VarFile("Base folder", new File(System.getProperty("user.home")));
+    
+    VarString folderName   = new VarString("Folder name", "myFolder");
+    
+    VarFile   folder       = new VarFile("New folder", null);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        parentFolder.setDefaultEditorModel(new FileTypeModel(parentFolder.getValue().getPath(), FileMode.FOLDERS, null, true));
+        inputMap.add("parent folder", parentFolder);
+        inputMap.add("folder name", folderName);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("new folder", folder);
+    }
+    
+    @Override
+    public void run()
+    {
+        File parent = parentFolder.getValue();
+        
+        if (parent == null || !parent.exists() || !parent.isDirectory()) throw new VarException(parentFolder, "The parent folder does not exist or is not a folder");
+        
+        if (folderName.getValue().isEmpty()) throw new VarException(folderName, "The name of the folder cannot be empty");
+        
+        File newFolder = new File(parentFolder + File.separator + folderName.getValue());
+        
+        if (newFolder.exists() && !newFolder.isDirectory())
+        {
+            throw new VarException(folderName, "Cannot create folder " + newFolder.getPath() + "\n=> A file with this name already exists");
+        }
+        
+        FileUtil.createDir(newFolder);
+        
+        folder.setValue(newFolder);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/FileToPath.java b/src/main/java/plugins/adufour/blocks/tools/io/FileToPath.java
new file mode 100644
index 0000000000000000000000000000000000000000..e38996efcc8b19208647375ebbc7a8882f68d700
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/FileToPath.java
@@ -0,0 +1,31 @@
+package plugins.adufour.blocks.tools.io;
+
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarString;
+
+public class FileToPath extends Plugin implements IOBlock
+{
+    VarString path = new VarString("path", "", 1);
+    
+    VarFile   file = new VarFile("file", null);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input file", file);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output path", path);
+    }
+    
+    @Override
+    public void run()
+    {
+        path.setValue(file.getValue() == null ? null : file.getValue().getPath());
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/FileToSequence.java b/src/main/java/plugins/adufour/blocks/tools/io/FileToSequence.java
new file mode 100644
index 0000000000000000000000000000000000000000..93c386a703e3f01bfbf333f748e1923affea03ca
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/FileToSequence.java
@@ -0,0 +1,56 @@
+package plugins.adufour.blocks.tools.io;
+
+import icy.file.Loader;
+import icy.plugin.abstract_.Plugin;
+import icy.sequence.Sequence;
+
+import java.io.File;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarInteger;
+import plugins.adufour.vars.lang.VarSequence;
+import plugins.adufour.vars.util.VarException;
+
+/**
+ * Utility block that reads a file from disk
+ * 
+ * @author Alexandre Dufour
+ */
+public class FileToSequence extends Plugin implements IOBlock
+{
+    VarFile     inputFile      = new VarFile("input file", null);
+    VarSequence outputSequence = new VarSequence("sequence", null);
+    VarInteger  series         = new VarInteger("Series", 0);
+    
+    @Override
+    public void run()
+    {
+        try
+        {
+            File file = inputFile.getValue(true);
+            Sequence sequence = Loader.loadSequence(file.getPath(), series.getValue(), false);
+            if (sequence == null) throw new VarException(inputFile, "Cannot read " + file.getPath() + " into a sequence");
+            outputSequence.setValue(sequence);
+        }
+        catch (Exception e)
+        {
+            File file = inputFile.getValue(true);
+            throw new VarException(inputFile, "unable to read file " + file.getAbsolutePath());
+        }
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input file", inputFile);
+        inputMap.add("Series", series);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("sequence", outputSequence);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/FilesToSequence.java b/src/main/java/plugins/adufour/blocks/tools/io/FilesToSequence.java
new file mode 100644
index 0000000000000000000000000000000000000000..3dc196d2d19f0358bc7b91299d1d2bdd4f793c4a
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/FilesToSequence.java
@@ -0,0 +1,57 @@
+package plugins.adufour.blocks.tools.io;
+
+import icy.file.Loader;
+import icy.plugin.abstract_.Plugin;
+import icy.sequence.Sequence;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarFileArray;
+import plugins.adufour.vars.lang.VarSequence;
+import plugins.adufour.vars.util.VarException;
+
+/**
+ * Utility block that reads multiple image files from disk into a {@link Sequence}
+ * 
+ * @author Alexandre Dufour
+ */
+public class FilesToSequence extends Plugin implements IOBlock
+{
+    VarFileArray inputFiles     = new VarFileArray("input file", new File[] {});
+    VarSequence  outputSequence = new VarSequence("sequence", null);
+    
+    @Override
+    public void run()
+    {
+        try
+        {
+            File[] files = inputFiles.getValue(true);
+            List<String> paths = new ArrayList<String>(files.length);
+
+            for (int i = 0; i < files.length; i++)
+                if (files[i] != null) paths.add(files[i].getPath());
+            
+            outputSequence.setValue(Loader.loadSequence(paths, false));
+        }
+        catch (Exception e)
+        {
+            throw new VarException(inputFiles, "unable to read files");
+        }
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input file", inputFiles);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("sequence", outputSequence);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/GetSequenceFolder.java b/src/main/java/plugins/adufour/blocks/tools/io/GetSequenceFolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..5cfbf8670df3db545c49070b3973102be3b3082c
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/GetSequenceFolder.java
@@ -0,0 +1,40 @@
+package plugins.adufour.blocks.tools.io;
+
+import java.io.File;
+
+import icy.file.FileUtil;
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.tools.sequence.SequenceBlock;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarSequence;
+import plugins.adufour.vars.util.VarException;
+
+public class GetSequenceFolder extends Plugin implements SequenceBlock, IOBlock
+{
+    VarSequence sequence = new VarSequence("Sequence", null);
+    
+    VarFile folder = new VarFile("Folder", null);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("Sequence", sequence);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("Folder", folder);
+    }
+    
+    @Override
+    public void run()
+    {
+        String filePath = sequence.getValue(true).getFilename();
+        if (filePath == null) throw new VarException(sequence, "[Get sequence folder]: the selected sequence has not been saved on disk");
+        
+        String folderPath = FileUtil.getDirectory(filePath);
+        folder.setValue(new File(folderPath));
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/IOBlock.java b/src/main/java/plugins/adufour/blocks/tools/io/IOBlock.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c8eab2c005473f10d630b139a8c824b1698a677
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/IOBlock.java
@@ -0,0 +1,14 @@
+package plugins.adufour.blocks.tools.io;
+
+import plugins.adufour.blocks.lang.Block;
+
+/**
+ * Interface used to mark blocks as I/O blocks (and appear in the appropriate menu)
+ * 
+ * @author Alexandre Dufour
+ * 
+ */
+public interface IOBlock extends Block
+{
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/PathToFile.java b/src/main/java/plugins/adufour/blocks/tools/io/PathToFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1a115542e9067da18104d0eda55a9444e1268b9
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/PathToFile.java
@@ -0,0 +1,34 @@
+package plugins.adufour.blocks.tools.io;
+
+import icy.plugin.abstract_.Plugin;
+
+import java.io.File;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarString;
+
+public class PathToFile extends Plugin implements IOBlock
+{
+    VarString path = new VarString("path", "", 1);
+    
+    VarFile   file = new VarFile("file", null);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input path", path);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output file", file);
+    }
+    
+    @Override
+    public void run()
+    {
+        file.setValue(new File(path.getValue(true)));
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/SendToSwimmingPool.java b/src/main/java/plugins/adufour/blocks/tools/io/SendToSwimmingPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ebfcd0077e4bd432bd21bada1d48b2719ef4b49
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/SendToSwimmingPool.java
@@ -0,0 +1,47 @@
+package plugins.adufour.blocks.tools.io;
+
+import javax.swing.SwingUtilities;
+
+import icy.main.Icy;
+import icy.plugin.abstract_.Plugin;
+import icy.swimmingPool.SwimmingObject;
+import plugins.adufour.blocks.lang.Block;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarMutable;
+
+/**
+ * Sends any data to the swimming pool
+ * 
+ * @author Alexandre Dufour
+ */
+public class SendToSwimmingPool extends Plugin implements Block
+{
+    private VarMutable data = new VarMutable("Data", null);
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("Data", data);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+    @Override
+    public void run()
+    {
+        if (data.getValue() == null) return;
+        
+        SwingUtilities.invokeLater(new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                Icy.getMainInterface().getSwimmingPool().add(new SwimmingObject(data.getValue(), data.getReference().getName()));
+            }
+        });
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/io/SequenceToFile.java b/src/main/java/plugins/adufour/blocks/tools/io/SequenceToFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3c4a87c9e440be415c3cfa9bb956b80257ff770
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/io/SequenceToFile.java
@@ -0,0 +1,47 @@
+package plugins.adufour.blocks.tools.io;
+
+import icy.file.Saver;
+import icy.plugin.abstract_.Plugin;
+
+import java.io.File;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.FileMode;
+import plugins.adufour.vars.gui.model.FileTypeModel;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarSequence;
+
+/**
+ * Utility block that saves a sequence to disk
+ * 
+ * @author Alexandre Dufour
+ */
+public class SequenceToFile extends Plugin implements IOBlock
+{
+    private VarFile     folder   = new VarFile("File or folder", null);
+    private VarSequence sequence = new VarSequence("sequence", null);
+    
+    @Override
+    public void run()
+    {
+        File f = new File(folder.getValue(true).getAbsolutePath());
+        if (f.isDirectory())
+        {
+            f = new File(f.getAbsolutePath() + File.separator + sequence.getValue(true) + ".tif");
+        }
+        Saver.save(sequence.getValue(true), f, false, false);
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        folder.setDefaultEditorModel(new FileTypeModel("", FileMode.FOLDERS, null, false));
+        inputMap.add("folder", folder);
+        inputMap.add("sequence", sequence);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/AddROIToSequence.java b/src/main/java/plugins/adufour/blocks/tools/roi/AddROIToSequence.java
new file mode 100644
index 0000000000000000000000000000000000000000..231821d1a7016e8ff2d95ea4a5fad024b4c50cc3
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/AddROIToSequence.java
@@ -0,0 +1,56 @@
+package plugins.adufour.blocks.tools.roi;
+
+import icy.plugin.abstract_.Plugin;
+import icy.roi.ROI;
+import icy.sequence.Sequence;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarBoolean;
+import plugins.adufour.vars.lang.VarROIArray;
+import plugins.adufour.vars.lang.VarSequence;
+
+/**
+ * (Deprecated: Use plugins.tprovoost.sequenceblocks.add.AddRois instead)
+ * Block to add one or several {@link ROI} to a Sequence.
+ * 
+ * class: plugins.adufour.blacks.tools.roi.AddROIToSequence
+ * 
+ * @author Alexandre Dufour
+ */
+@Deprecated
+public class AddROIToSequence extends Plugin implements ROIBlock
+{
+    VarROIArray rois     = new VarROIArray("ROI to add");
+    
+    VarSequence sequence = new VarSequence("Source", null);
+    
+    VarBoolean  replace  = new VarBoolean("Overwrite", false);
+    
+    @Override
+    public void run()
+    {
+        Sequence s = sequence.getValue(true);
+        
+        if (replace.getValue()) s.removeAllROI();
+        
+        s.beginUpdate();
+        for (ROI roi : rois.getValue(true))
+            s.addROI(roi);
+        s.endUpdate();
+        
+        s.saveXMLData();
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("target sequence", sequence);
+        inputMap.add("input rois", rois);
+        inputMap.add("replace existing", replace);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/CropSequenceToROI.java b/src/main/java/plugins/adufour/blocks/tools/roi/CropSequenceToROI.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa22cb857e8e77798e5b527d974c085f4ee7db91
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/CropSequenceToROI.java
@@ -0,0 +1,114 @@
+package plugins.adufour.blocks.tools.roi;
+
+import icy.image.IcyBufferedImage;
+import icy.image.IcyBufferedImageUtil;
+import icy.plugin.abstract_.Plugin;
+import icy.roi.ROI;
+import icy.sequence.Sequence;
+import icy.type.rectangle.Rectangle5D;
+import icy.util.OMEUtil;
+import icy.util.StringUtil;
+
+import java.awt.Rectangle;
+import java.util.ArrayList;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarGenericArray;
+import plugins.adufour.vars.lang.VarROIArray;
+import plugins.adufour.vars.lang.VarSequence;
+
+public class CropSequenceToROI extends Plugin implements ROIBlock
+{
+    VarSequence                 input  = new VarSequence("Sequence to crop", null);
+    
+    VarROIArray                 rois   = new VarROIArray("List of ROI");
+    
+    VarGenericArray<Sequence[]> output = new VarGenericArray<Sequence[]>("List of crops", Sequence[].class, new Sequence[] {});
+    
+    @Override
+    public void run()
+    {
+        Sequence seq = input.getValue(true);
+        
+        int nbROIs = rois.getValue(true).length;
+        
+        ArrayList<Sequence> crops = new ArrayList<Sequence>(nbROIs);
+        
+        int cpt = 1;
+        int digitSize = 1 + (int) Math.log10(nbROIs);
+        
+        for (ROI roi : rois.getValue())
+        {
+            final Sequence result = new Sequence(OMEUtil.createOMEXMLMetadata(seq.getOMEXMLMetadata()));
+            
+            Rectangle5D.Integer region = roi.getBounds5D().toInteger();
+            
+            final Rectangle region2d = region.toRectangle2D().getBounds();
+            final int startZ;
+            final int endZ;
+            final int startT;
+            final int endT;
+            
+            if (region.isInfiniteZ())
+            {
+                startZ = 0;
+                endZ = seq.getSizeZ();
+            }
+            else
+            {
+                startZ = Math.max(0, region.z);
+                endZ = Math.min(seq.getSizeZ(), region.z + region.sizeZ);
+            }
+            if (region.isInfiniteT())
+            {
+                startT = 0;
+                endT = seq.getSizeT();
+            }
+            else
+            {
+                startT = Math.max(0, region.t);
+                endT = (int) Math.min(seq.getSizeT(), (long) region.t + (long) region.sizeT);
+            }
+            
+            result.beginUpdate();
+            try
+            {
+                for (int t = startT; t < endT; t++)
+                {
+                    for (int z = startZ; z < endZ; z++)
+                    {
+                        IcyBufferedImage img = seq.getImage(t, z);
+                        
+                        if (img != null) img = IcyBufferedImageUtil.getSubImage(img, region2d, region.c, region.sizeC);
+                        
+                        result.setImage(t - startT, z - startZ, img);
+                    }
+                }
+            }
+            finally
+            {
+                result.endUpdate();
+            }
+            
+            result.setName(seq.getName() + "_crop" + StringUtil.toString(cpt, digitSize));
+            crops.add(result);
+            cpt++;
+        }
+        
+        output.setValue(crops.toArray(new Sequence[crops.size()]));
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("sequence to crop", input);
+        inputMap.add("list of ROI", rois);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("list of crops", output);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/DilateROI.java b/src/main/java/plugins/adufour/blocks/tools/roi/DilateROI.java
new file mode 100644
index 0000000000000000000000000000000000000000..1a984cd18fcbbd55eb2fdb10625c416e6c8fa072
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/DilateROI.java
@@ -0,0 +1,265 @@
+package plugins.adufour.blocks.tools.roi;
+
+import java.awt.Point;
+import java.util.ArrayList;
+
+import icy.roi.BooleanMask2D;
+import icy.roi.BooleanMask3D;
+import icy.roi.ROI;
+import icy.roi.ROI2D;
+import icy.roi.ROI3D;
+import icy.type.point.Point3D;
+import plugins.adufour.vars.lang.VarROIArray;
+import plugins.adufour.vars.util.VarException;
+import plugins.kernel.roi.roi2d.ROI2DArea;
+import plugins.kernel.roi.roi3d.ROI3DArea;
+
+public class DilateROI extends MorphROI
+{
+    public DilateROI()
+    {
+        roiOUT = new VarROIArray("Dilated ROI");
+    }
+
+    @Override
+    public void run()
+    {
+        switch (unit.getValue())
+        {
+            case PIXELS:
+                roiOUT.setValue(dilateROI(roiIN.getValue(), x.getValue(), y.getValue(), z.getValue()));
+                break;
+            case PERCENTAGE:
+                roiOUT.setValue(dilateROIByPercentage(roiIN.getValue(), x.getValue(), y.getValue(), z.getValue()));
+                break;
+            default:
+                throw new VarException(unit, "Unsupported unit");
+        }
+    }
+
+    /**
+     * Perform a morphological dilation on the specified set of ROI by the given amount in each
+     * dimension
+     * 
+     * @param inputRoi
+     *        The ROI to dilate
+     * @param xRadius
+     *        the radius (in pixels) along X
+     * @param yRadius
+     *        the radius (in pixels) along X
+     * @param zRadius
+     *        the radius (in pixels) along Z (not used if <code>roi</code> is 2D)
+     * @return a new set of dilated ROI of type "area"
+     */
+    public static ROI[] dilateROI(ROI[] inputRoi, int xRadius, int yRadius, int zRadius)
+    {
+        ArrayList<ROI> out = new ArrayList<ROI>(inputRoi.length);
+
+        for (ROI roi : inputRoi)
+        {
+            if (Thread.currentThread().isInterrupted())
+                break;
+
+            ROI dilated = dilateROI(roi, xRadius, yRadius, zRadius);
+            if (dilated != null)
+                out.add(dilated);
+        }
+
+        return out.toArray(new ROI[out.size()]);
+    }
+
+    /**
+     * Perform a morphological dilation on the specified set of ROI by the given percentage in each
+     * dimension
+     * 
+     * @param inputRoi
+     *        the ROI to dilate
+     * @param xPct
+     *        the percentage (from 0 to 100) to dilate along X
+     * @param yPct
+     *        the percentage (from 0 to 100) to dilate along Y
+     * @param zPct
+     *        the percentage (from 0 to 100) to dilate along Z (not used in 2D)
+     * @return a new set of dilated ROI of type "area"
+     */
+    public static ROI[] dilateROIByPercentage(ROI[] inputRoi, int xPct, int yPct, int zPct)
+    {
+        ArrayList<ROI> out = new ArrayList<ROI>(inputRoi.length);
+
+        for (ROI roi : inputRoi)
+        {
+            if (Thread.currentThread().isInterrupted())
+                break;
+
+            ROI dilated = dilateROIByPercentage(roi, xPct, yPct, zPct);
+            if (dilated != null)
+                out.add(dilated);
+        }
+
+        return out.toArray(new ROI[out.size()]);
+    }
+
+    /**
+     * Perform a morphological dilation on the specified ROI by the given percentage in each
+     * dimension
+     * 
+     * @author Joel Rogers, Alexandre Dufour
+     * @param roi
+     *        the ROI to dilate
+     * @param xPct
+     *        the percentage (from 0 to 100) to dilate along X
+     * @param yPct
+     *        the percentage (from 0 to 100) to dilate along Y
+     * @param zPct
+     *        the percentage (from 0 to 100) to dilate along Z (not used in 2D)
+     * @return a new, dilated ROI of type "area"
+     */
+    public static ROI dilateROIByPercentage(ROI roi, int xPct, int yPct, int zPct)
+    {
+        int xRadius = percentageToRadiusX(roi, xPct);
+        int yRadius = percentageToRadiusY(roi, yPct);
+        int zRadius = percentageToRadiusZ(roi, zPct);
+
+        return dilateROI(roi, xRadius, yRadius, zRadius);
+    }
+
+    /**
+     * Perform a morphological dilation on the specified ROI by the given radius in each dimension
+     * 
+     * @param roi
+     *        the ROI to dilate
+     * @param xRadius
+     *        the radius in pixels along X
+     * @param yRadius
+     *        the radius in pixels along X
+     * @param zRadius
+     *        the radius in pixels along Z (not used if <code>roi</code> is 2D)
+     * @return a new, dilated ROI of type "area"
+     */
+    public static ROI dilateROI(ROI roi, int xRadius, int yRadius, int zRadius)
+    {
+        int rx = xRadius, rrx = rx * rx;
+        int ry = yRadius, rry = ry * ry;
+        int rz = zRadius, rrz = rz * rz;
+
+        if (roi instanceof ROI2D)
+        {
+            BooleanMask2D m2 = ((ROI2D) roi).getBooleanMask(true);
+            ROI2DArea r2 = new ROI2DArea(m2);
+            r2.setC(((ROI2D) roi).getC());
+            r2.setZ(((ROI2D) roi).getZ());
+            r2.setT(((ROI2D) roi).getT());
+            r2.setName(roi.getName() + " dilated[" + xRadius + "," + yRadius + "]");
+
+            r2.beginUpdate();
+
+            for (Point p : m2.getContourPoints())
+            {
+                // Brute force
+                for (int y = -ry; y <= ry; y++)
+                    for (int x = -rx; x <= rx; x++)
+                    {
+                        double xr2 = rrx == 0 ? 0 : x * x / rrx;
+                        double yr2 = rry == 0 ? 0 : y * y / rry;
+
+                        if (xr2 + yr2 <= 1.0)
+                        {
+                            if (!m2.contains(p.x + x, p.y + y))
+                                r2.addPoint(p.x + x, p.y + y);
+                        }
+                    }
+
+                // Bresenham style
+                // int x = r;
+                // int y = 0;
+                // int xChange = 1 - (r << 1);
+                // int yChange = 0;
+                // int radiusError = 0;
+                //
+                // while (x >= y)
+                // {
+                // for (int i = p.x - x; i <= p.x + x; i++)
+                // {
+                // if (!m2.contains(i, p.y + y)) r2.addPoint(i, p.y + y);
+                // if (!m2.contains(i, p.y - y)) r2.addPoint(i, p.y - y);
+                // }
+                // for (int i = p.x - y; i <= p.x + y; i++)
+                // {
+                // if (!m2.contains(i, p.y + x)) r2.addPoint(i, p.y + x);
+                // if (!m2.contains(i, p.y - x)) r2.addPoint(i, p.y - x);
+                // }
+                //
+                // y++;
+                // radiusError += yChange;
+                // yChange += 2;
+                // if (((radiusError << 1) + xChange) > 0)
+                // {
+                // x--;
+                // radiusError += xChange;
+                // xChange += 2;
+                // }
+                // }
+            }
+            r2.endUpdate();
+
+            return r2;
+        }
+        else if (roi instanceof ROI3D)
+        {
+            ROI3D roi3D = (ROI3D) roi;
+
+            BooleanMask3D m3 = roi3D.getBooleanMask(true);
+
+            ROI3DArea r3 = new ROI3DArea(m3);
+            r3.setC(((ROI3D) roi).getC());
+            r3.setT(((ROI3D) roi).getT());
+            r3.setName(roi.getName() + " dilated[" + xRadius + "," + yRadius + "," + zRadius + "]");
+
+            r3.beginUpdate();
+
+            for (Point3D.Integer p : m3.getContourPoints())
+            { // Brute force
+
+                if (rrz == 0)
+                {
+                    int z = 0;
+                    for (int y = -ry; y <= ry; y++)
+                        for (int x = -rx; x <= rx; x++)
+                        {
+                            double xr2 = rrx == 0 ? 0 : x * x / rrx;
+                            double yr2 = rry == 0 ? 0 : y * y / rry;
+
+                            if (xr2 + yr2 <= 1.0)
+                            {
+                                if (!m3.contains(p.x + x, p.y + y, p.z + z))
+                                    r3.addPoint(p.x + x, p.y + y, p.z + z);
+                            }
+                        }
+                }
+                else
+                {
+                    for (int z = -rz; z <= rz; z++)
+                        for (int y = -ry; y <= ry; y++)
+                            for (int x = -rx; x <= rx; x++)
+                            {
+                                double xr2 = rrx == 0 ? 0 : x * x / rrx;
+                                double yr2 = rry == 0 ? 0 : y * y / rry;
+                                double zr2 = rrz == 0 ? 0 : z * z / rrz;
+
+                                if (xr2 + yr2 + zr2 <= 1.0)
+                                {
+                                    if (!m3.contains(p.x + x, p.y + y, p.z + z))
+                                        r3.addPoint(p.x + x, p.y + y, p.z + z);
+                                }
+                            }
+                }
+            }
+            r3.endUpdate();
+            r3.optimizeBounds();
+            return r3;
+        }
+
+        System.out.println("[Dilate ROI] Warning: unsupported ROI: " + roi.getName());
+        return null;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/ErodeROI.java b/src/main/java/plugins/adufour/blocks/tools/roi/ErodeROI.java
new file mode 100644
index 0000000000000000000000000000000000000000..aa0fc7d76665454ab9b2eaabab45fca4f66ae9eb
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/ErodeROI.java
@@ -0,0 +1,259 @@
+package plugins.adufour.blocks.tools.roi;
+
+import java.awt.Point;
+import java.util.ArrayList;
+
+import icy.roi.BooleanMask2D;
+import icy.roi.BooleanMask3D;
+import icy.roi.ROI;
+import icy.roi.ROI2D;
+import icy.roi.ROI3D;
+import icy.type.point.Point3D;
+import plugins.adufour.vars.lang.VarROIArray;
+import plugins.adufour.vars.util.VarException;
+import plugins.kernel.roi.roi2d.ROI2DArea;
+import plugins.kernel.roi.roi3d.ROI3DArea;
+
+public class ErodeROI extends MorphROI
+{
+    public ErodeROI()
+    {
+        roiOUT = new VarROIArray("Eroded ROI");
+    }
+
+    @Override
+    public void run()
+    {
+        switch (unit.getValue())
+        {
+            case PIXELS:
+                roiOUT.setValue(erodeROI(roiIN.getValue(), x.getValue(), y.getValue(), z.getValue()));
+                break;
+            case PERCENTAGE:
+                roiOUT.setValue(erodeROIByPercentage(roiIN.getValue(), x.getValue(), y.getValue(), z.getValue()));
+                break;
+            default:
+                throw new VarException(unit, "Unsupported unit");
+        }
+    }
+
+    /**
+     * Perform a morphological erosion on the specified set of ROI by the given radius in each
+     * dimension
+     * 
+     * @param inputRoi
+     *        the ROI to erode
+     * @param xRadius
+     *        the radius (in pixels) along X
+     * @param yRadius
+     *        the radius (in pixels) along Y
+     * @param zRadius
+     *        the radius (in pixels) along Z (not used if <code>roi</code> is 2D)
+     * @return a new set of eroded ROI of type "area"
+     */
+    public static ROI[] erodeROI(ROI[] inputRoi, int xRadius, int yRadius, int zRadius)
+    {
+        ArrayList<ROI> out = new ArrayList<ROI>(inputRoi.length);
+
+        for (ROI roi : inputRoi)
+        {
+            if (Thread.currentThread().isInterrupted())
+                break;
+
+            ROI eroded = erodeROI(roi, xRadius, yRadius, zRadius);
+            if (eroded != null)
+                out.add(eroded);
+        }
+
+        return out.toArray(new ROI[out.size()]);
+    }
+
+    /**
+     * Perform a morphological erosion on the specified set of ROI by the given scale factor in each
+     * dimension
+     * 
+     * @param inputRoi
+     *        the ROI to erode
+     * @param xPct
+     *        the percentage (from 0 to 100) to erode along X
+     * @param yPct
+     *        the percentage (from 0 to 100) to erode along Y
+     * @param zPct
+     *        the percentage (from 0 to 100) to erode along Z (not used in 2D)
+     * @return a new set of eroded ROI of type "area"
+     */
+    public static ROI[] erodeROIByPercentage(ROI[] inputRoi, int xPct, int yPct, int zPct)
+    {
+        ArrayList<ROI> out = new ArrayList<ROI>(inputRoi.length);
+
+        for (ROI roi : inputRoi)
+        {
+            if (Thread.currentThread().isInterrupted())
+                break;
+
+            ROI eroded = erodeROIByPercentage(roi, xPct, yPct, zPct);
+            if (eroded != null)
+                out.add(eroded);
+        }
+
+        return out.toArray(new ROI[out.size()]);
+    }
+
+    /**
+     * Perform a morphological erosion on the specified ROI by the given scale factor in each
+     * dimension
+     * 
+     * @author Joel Rogers, Alexandre Dufour
+     * @param roi
+     *        the ROI to erode
+     * @param xPct
+     *        the percentage (from 0 to 100) to dilate along X
+     * @param yPct
+     *        the percentage (from 0 to 100) to dilate along Y
+     * @param zPct
+     *        the percentage (from 0 to 100) to dilate along Z (not used in 2D)
+     * @return a new, dilated ROI of type "area"
+     */
+    public static ROI erodeROIByPercentage(ROI roi, int xPct, int yPct, int zPct)
+    {
+        int xRadius = percentageToRadiusX(roi, xPct);
+        int yRadius = percentageToRadiusY(roi, yPct);
+        int zRadius = percentageToRadiusZ(roi, zPct);
+
+        return erodeROI(roi, xRadius, yRadius, zRadius);
+    }
+
+    /**
+     * Perform a morphological erosion on the specified ROI by the given radius in each dimension
+     * 
+     * @param roi
+     *        the ROI to erode
+     * @param xRadius
+     *        the radius in pixels along X
+     * @param yRadius
+     *        the radius in pixels along X
+     * @param zRadius
+     *        the radius in pixels along Z (not used if <code>roi</code> is 2D)
+     * @return a new, eroded ROI of type "area"
+     */
+    public static ROI erodeROI(ROI roi, int xRadius, int yRadius, int zRadius)
+    {
+        // The basis of this erosion operator is to remove all pixels within a distance of "radius"
+        // from the border. Since we have easy access to the contour points of the ROI, we will
+        // start from there and instead use a radius of "radius - 1" when searching for pixels to
+        // erase, so as to be consistent with the dual dilation operator, such that openings
+        // (erosion + dilation) and closings (dilation + erosion) preserve the global ROI size
+
+        int rx = Math.max(0, xRadius - 1), rrx = rx * rx;
+        int ry = Math.max(0, yRadius - 1), rry = ry * ry;
+        int rz = Math.max(0, zRadius - 1), rrz = rz * rz;
+
+        if (roi instanceof ROI2D)
+        {
+            BooleanMask2D m2 = ((ROI2D) roi).getBooleanMask(true);
+            ROI2DArea r2 = new ROI2DArea(m2);
+            r2.setC(((ROI2D) roi).getC());
+            r2.setZ(((ROI2D) roi).getZ());
+            r2.setT(((ROI2D) roi).getT());
+            r2.setName(roi.getName() + " eroded[" + xRadius + "," + yRadius + "]");
+
+            r2.beginUpdate();
+
+            for (Point p : m2.getContourPoints())
+            {
+                // Brute force
+                for (int y = -ry; y <= ry; y++)
+                    for (int x = -rx; x <= rx; x++)
+                    {
+                        double xr2 = rrx == 0 ? 0 : x * x / rrx;
+                        double yr2 = rry == 0 ? 0 : y * y / rry;
+
+                        // correct the sphere equation to include the outer rim for each pixel
+                        if (xr2 + yr2 <= 2.0)
+                        {
+                            if (m2.contains(p.x + x, p.y + y))
+                                r2.removePoint(p.x + x, p.y + y);
+                        }
+                    }
+            }
+            r2.endUpdate();
+
+            return r2.getNumberOfPoints() > 0 ? r2 : null;
+        }
+        else if (roi instanceof ROI3D)
+        {
+            ROI3D roi3D = (ROI3D) roi;
+
+            BooleanMask3D m3 = roi3D.getBooleanMask(true);
+
+            ROI3DArea r3 = new ROI3DArea(m3);
+            r3.setC(((ROI3D) roi).getC());
+            r3.setT(((ROI3D) roi).getT());
+            r3.setName(roi.getName() + " eroded[" + xRadius + "," + yRadius + "," + zRadius + "]");
+
+            r3.beginUpdate();
+
+            for (Point3D.Integer p : m3.getContourPoints())
+            { // Brute force
+
+                if (rrz == 0)
+                {
+                    int z = 0;
+                    for (int y = -ry; y <= ry; y++)
+                        for (int x = -rx; x <= rx; x++)
+                        {
+                            double xr2 = rrx == 0 ? 0 : x * x / rrx;
+                            double yr2 = rry == 0 ? 0 : y * y / rry;
+
+                            // correct the sphere equation to include the outer rim for each pixel
+                            if (xr2 + yr2 <= 2.0)
+                            {
+                                if (m3.contains(p.x + x, p.y + y, p.z + z))
+                                    try
+                                    {
+                                        r3.removePoint(p.x + x, p.y + y, p.z + z);
+                                    }
+                                    catch (ArrayIndexOutOfBoundsException aioobe)
+                                    {
+                                        // FIXME @Stephane, what's wrong here?!
+                                    }
+                            }
+                        }
+                }
+                else
+                {
+                    for (int z = -rz; z <= rz; z++)
+                        for (int y = -ry; y <= ry; y++)
+                            for (int x = -rx; x <= rx; x++)
+                            {
+                                double xr2 = rrx == 0 ? 0 : x * x / rrx;
+                                double yr2 = rry == 0 ? 0 : y * y / rry;
+                                double zr2 = rrz == 0 ? 0 : z * z / rrz;
+
+                                // correct the sphere equation to include the outer rim for each
+                                // pixel
+                                if (xr2 + yr2 + zr2 <= 2.0)
+                                {
+                                    if (m3.contains(p.x + x, p.y + y, p.z + z))
+                                        try
+                                        {
+                                            r3.removePoint(p.x + x, p.y + y, p.z + z);
+                                        }
+                                        catch (ArrayIndexOutOfBoundsException aioobe)
+                                        {
+                                            // FIXME @Stephane, what's wrong here?!
+                                        }
+                                }
+                            }
+                }
+            }
+            r3.endUpdate();
+            r3.optimizeBounds();
+
+            return r3.getNumberOfPoints() > 0 ? r3 : null;
+        }
+
+        System.out.println("[Erode ROI] Warning: unsupported ROI: " + roi.getName());
+        return null;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/GetROIFromSequence.java b/src/main/java/plugins/adufour/blocks/tools/roi/GetROIFromSequence.java
new file mode 100644
index 0000000000000000000000000000000000000000..29024d658e29f767ee42a2ccf6de211f1a7d47a0
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/GetROIFromSequence.java
@@ -0,0 +1,72 @@
+package plugins.adufour.blocks.tools.roi;
+
+import icy.plugin.abstract_.Plugin;
+import icy.roi.ROI;
+import icy.sequence.Sequence;
+
+import java.util.ArrayList;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarEnum;
+import plugins.adufour.vars.lang.VarROIArray;
+import plugins.adufour.vars.lang.VarSequence;
+
+public class GetROIFromSequence extends Plugin implements ROIBlock
+{
+    public enum ROIFilter
+    {
+        SELECTED, NON_SELECTED, ALL
+    }
+    
+    VarEnum<ROIFilter> filter   = new VarEnum<ROIFilter>("ROI to get", ROIFilter.ALL);
+    
+    VarROIArray        rois     = new VarROIArray("List of ROI");
+    
+    VarSequence        sequence = new VarSequence("Source", null);
+    
+    @Override
+    public void run()
+    {
+        Sequence s = sequence.getValue();
+        
+        if (s == null)
+        {
+            rois.setValue(new ROI[] {});
+            return;
+        }
+        
+        ArrayList<ROI> inputROI = s.getROIs();
+        
+        switch (filter.getValue())
+        {
+        case SELECTED:
+            for (int i = 0; i < inputROI.size(); i++)
+                if (!inputROI.get(i).isSelected()) inputROI.remove(i--);
+            break;
+        case NON_SELECTED:
+            for (int i = 0; i < inputROI.size(); i++)
+                if (inputROI.get(i).isSelected()) inputROI.remove(i--);
+            break;
+        case ALL:
+            break;
+        
+        default:
+            throw new UnsupportedOperationException("Unknown ROI selection option: " + filter.getValueAsString());
+        }
+        
+        rois.setValue(inputROI.toArray(new ROI[inputROI.size()]));
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input sequence", sequence);
+        inputMap.add("selection state of ROI to extract", filter);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("extracted rois", rois);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/MergeROI.java b/src/main/java/plugins/adufour/blocks/tools/roi/MergeROI.java
new file mode 100644
index 0000000000000000000000000000000000000000..07c51d5c0c080ce621c5cf97cf13b2739cdbcdb3
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/MergeROI.java
@@ -0,0 +1,47 @@
+package plugins.adufour.blocks.tools.roi;
+
+import java.util.Arrays;
+import java.util.List;
+
+import icy.plugin.abstract_.Plugin;
+import icy.roi.ROI;
+import icy.roi.ROIUtil;
+import icy.util.ShapeUtil.BooleanOperator;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarEnum;
+import plugins.adufour.vars.lang.VarROIArray;
+
+public class MergeROI extends Plugin implements ROIBlock
+{
+    VarEnum<BooleanOperator> operation = new VarEnum<BooleanOperator>("Merge operation", BooleanOperator.AND);
+    
+    VarROIArray              roiIn     = new VarROIArray("List of ROI");
+    
+    VarROIArray              roiOut    = new VarROIArray("Merged ROI");
+    
+    @Override
+    public void run()
+    {
+        roiOut.setValue(new ROI[0]);
+        
+        List<ROI> rois = Arrays.asList(roiIn.getValue());
+        
+        ROI merge = ROIUtil.merge(rois, operation.getValue());
+        
+        if (merge != null) roiOut.add(merge);
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("List of ROI", roiIn);
+        inputMap.add("Merge operation", operation);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("Merged ROI", roiOut);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/MorphROI.java b/src/main/java/plugins/adufour/blocks/tools/roi/MorphROI.java
new file mode 100644
index 0000000000000000000000000000000000000000..8f5ee8d606ed173ec66654737b2a2409e49db79c
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/MorphROI.java
@@ -0,0 +1,115 @@
+package plugins.adufour.blocks.tools.roi;
+
+import icy.plugin.abstract_.Plugin;
+import icy.roi.ROI;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.gui.model.IntegerRangeModel;
+import plugins.adufour.vars.lang.VarEnum;
+import plugins.adufour.vars.lang.VarInteger;
+import plugins.adufour.vars.lang.VarROIArray;
+
+abstract class MorphROI extends Plugin implements ROIBlock
+{
+    protected enum MorphUnit
+    {
+        PIXELS("Radius (px)"), PERCENTAGE("Scale (%)");
+        
+        private final String displayText;
+        
+        private MorphUnit(String displayText)
+        {
+            this.displayText = displayText;
+        }
+        
+        @Override
+        public String toString()
+        {
+            return displayText;
+        }
+    }
+    
+    VarROIArray roiIN = new VarROIArray("List of ROI");
+    VarInteger  x     = new VarInteger("Along X", 1);
+    VarInteger  y     = new VarInteger("Along Y", 1);
+    VarInteger  z     = new VarInteger("Along Z", 1);
+    
+    VarEnum<MorphUnit> unit = new VarEnum<MorphUnit>("Unit", MorphUnit.PIXELS);
+    
+    VarROIArray roiOUT;
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        x.setDefaultEditorModel(new IntegerRangeModel(1, 0, 100, 1));
+        y.setDefaultEditorModel(new IntegerRangeModel(1, 0, 100, 1));
+        z.setDefaultEditorModel(new IntegerRangeModel(1, 0, 100, 1));
+        inputMap.add("input ROI", roiIN);
+        inputMap.add("X radius", x);
+        inputMap.add("Y radius", y);
+        inputMap.add("Z radius", z);
+        inputMap.add("unit", unit);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output ROI", roiOUT);
+    }
+    
+    /**
+     * Converts a scaling factor to a corresponding radius along the X axis (to the nearest pixel)
+     * 
+     * @author Joel Rogers, Alexandre Dufour
+     * @param roi
+     *            the ROI to scale
+     * @param pct
+     *            the percentage factor
+     * @return a radius that is directly usable with the
+     *         {@link ErodeROI#erodeROI(ROI, int, int, int)} and
+     *         {@link DilateROI#dilateROI(ROI, int, int, int)} methods
+     */
+    static int percentageToRadiusX(ROI roi, int pct)
+    {
+        double size = roi.getBounds5D().getSizeX();
+        double diff = (size * pct / 100);
+        return (int) Math.round(diff * 0.5);
+    }
+    
+    /**
+     * Converts a scaling factor to a corresponding radius along the Y axis (to the nearest pixel)
+     * 
+     * @author Joel Rogers, Alexandre Dufour
+     * @param roi
+     *            the ROI to rescale
+     * @param pct
+     *            the percentage factor
+     * @return a radius that is directly usable with the
+     *         {@link ErodeROI#erodeROI(ROI, int, int, int)} and
+     *         {@link DilateROI#dilateROI(ROI, int, int, int)} methods
+     */
+    static int percentageToRadiusY(ROI roi, int pct)
+    {
+        double size = roi.getBounds5D().getSizeY();
+        double diff = (size * pct / 100);
+        return (int) Math.round(diff * 0.5);
+    }
+    
+    /**
+     * Converts a scaling factor to a corresponding radius along the Z axis (to the nearest pixel)
+     * 
+     * @author Joel Rogers, Alexandre Dufour
+     * @param roi
+     *            the ROI to rescale
+     * @param pct
+     *            the percentage factor
+     * @return a radius that is directly usable with the
+     *         {@link ErodeROI#erodeROI(ROI, int, int, int)} and
+     *         {@link DilateROI#dilateROI(ROI, int, int, int)} methods
+     */
+    static int percentageToRadiusZ(ROI roi, int pct)
+    {
+        double size = roi.getBounds5D().getSizeZ();
+        double diff = (size * pct / 100);
+        return (int) Math.round(diff * 0.5);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/ROIBlock.java b/src/main/java/plugins/adufour/blocks/tools/roi/ROIBlock.java
new file mode 100644
index 0000000000000000000000000000000000000000..56b43f2a941438ef813d7c383f09a53a62e1fb0b
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/ROIBlock.java
@@ -0,0 +1,14 @@
+package plugins.adufour.blocks.tools.roi;
+
+import plugins.adufour.blocks.lang.Block;
+
+/**
+ * Interface used to flag ROI-related blocks
+ * 
+ * @author Alexandre Dufour
+ * 
+ */
+public interface ROIBlock extends Block
+{
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/SubtractROI.java b/src/main/java/plugins/adufour/blocks/tools/roi/SubtractROI.java
new file mode 100644
index 0000000000000000000000000000000000000000..3529353bbb19c60aedbe23f9dd6459745ebc79c2
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/SubtractROI.java
@@ -0,0 +1,66 @@
+package plugins.adufour.blocks.tools.roi;
+
+import java.util.ArrayList;
+
+import icy.plugin.abstract_.Plugin;
+import icy.roi.ROI;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarROIArray;
+
+public class SubtractROI extends Plugin implements ROIBlock
+{
+    VarROIArray roiA = new VarROIArray("List of ROI #1");
+
+    VarROIArray roiB = new VarROIArray("List of ROI #2");
+
+    VarROIArray roiOut = new VarROIArray("Subtracted ROI");
+
+    @Override
+    public void run()
+    {
+        roiOut.setValue(subtractROI(roiA.getValue(true), roiB.getValue(true)));
+    }
+
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("List of ROI #1", roiA);
+        inputMap.add("List of ROI #2", roiB);
+    }
+
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("subtraction output ROI", roiOut);
+    }
+
+    /**
+     * Subtracts a set of ROI from another
+     * 
+     * @param roiA
+     *        ROIs to be subtracted from.
+     * @param roiB
+     *        ROIs subtracted from the first group of ROIs.
+     * @return A - B. The group of ROIs resulting from the subtraction from the each element of the group A and each element of the group B.
+     */
+    public static ROI[] subtractROI(ROI[] roiA, ROI[] roiB)
+    {
+        ArrayList<ROI> out = new ArrayList<ROI>(roiA.length);
+
+        for (ROI a : roiA)
+        {
+            ROI subtraction = a.getCopy();
+
+            for (ROI b : roiB)
+                subtraction = subtraction.getSubtraction(b);
+
+            if (subtraction == null || subtraction.isEmpty())
+                continue;
+
+            if (!subtraction.getBounds5D().isEmpty())
+                out.add(subtraction);
+        }
+
+        return out.toArray(new ROI[out.size()]);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/roi/TranslateROI.java b/src/main/java/plugins/adufour/blocks/tools/roi/TranslateROI.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d20cf43e28e6d9596916d6ad53fe7af4dda32c9
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/roi/TranslateROI.java
@@ -0,0 +1,83 @@
+package plugins.adufour.blocks.tools.roi;
+
+import icy.plugin.abstract_.Plugin;
+import icy.roi.ROI;
+import icy.type.point.Point5D;
+
+import java.awt.geom.Point2D;
+
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarInteger;
+import plugins.adufour.vars.lang.VarROIArray;
+import plugins.kernel.roi.roi2d.ROI2DArea;
+import plugins.kernel.roi.roi3d.ROI3DArea;
+
+public class TranslateROI extends Plugin implements ROIBlock
+{
+    VarROIArray inputROI  = new VarROIArray("Input ROI");
+    
+    VarInteger  xShift    = new VarInteger("X shift", 0);
+    VarInteger  yShift    = new VarInteger("Y shift", 0);
+    VarInteger  zShift    = new VarInteger("Z shift", 0);
+    
+    VarROIArray outputROI = new VarROIArray("Translated ROI");
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input ROI", inputROI);
+        inputMap.add("X shift", xShift);
+        inputMap.add("Y shift", yShift);
+        inputMap.add("Z shift", zShift);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output ROI", outputROI);
+    }
+    
+    @Override
+    public void run()
+    {
+        outputROI.setValue(new ROI[0]);
+        
+        for (ROI roi : inputROI)
+        {
+            if (Thread.currentThread().isInterrupted()) break;
+            
+            if (roi.canSetPosition())
+            {
+                ROI shiftedROI = roi.getCopy();
+                Point5D pos = shiftedROI.getPosition5D();
+                pos.setX(pos.getX() + xShift.getValue());
+                pos.setY(pos.getY() + yShift.getValue());
+                pos.setZ(pos.getZ() + zShift.getValue());
+                shiftedROI.setPosition5D(pos);
+                outputROI.add(shiftedROI);
+            }
+            else
+            {
+                if (roi instanceof ROI3DArea)
+                {
+                    ROI3DArea shiftedROI = new ROI3DArea();
+                    
+                    for (ROI2DArea slice : (ROI3DArea) roi)
+                    {
+                        ROI2DArea sliceCopy = (ROI2DArea) slice.getCopy();
+                        
+                        Point2D p2 = slice.getPosition2D();
+                        p2.setLocation(p2.getX() + xShift.getValue(), p2.getY() + yShift.getValue());
+                        sliceCopy.setPosition2D(p2);
+                        
+                        shiftedROI.setSlice(slice.getZ() + zShift.getValue(), sliceCopy);
+                    }
+                    
+                    outputROI.add(shiftedROI);
+                }
+            }
+            
+        }
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/sequence/SequenceBlock.java b/src/main/java/plugins/adufour/blocks/tools/sequence/SequenceBlock.java
new file mode 100644
index 0000000000000000000000000000000000000000..bbc1f14cea37bc9380bdf416336ea6a9ed167403
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/sequence/SequenceBlock.java
@@ -0,0 +1,14 @@
+package plugins.adufour.blocks.tools.sequence;
+
+import plugins.adufour.blocks.lang.Block;
+
+/**
+ * Interface used to flag Sequence-related blocks
+ * 
+ * @author Alexandre Dufour
+ * 
+ */
+public interface SequenceBlock extends Block
+{
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/sequence/SequenceScreenshot.java b/src/main/java/plugins/adufour/blocks/tools/sequence/SequenceScreenshot.java
new file mode 100644
index 0000000000000000000000000000000000000000..f5ae733307ebc464303ca5bc795a3c3ec7f28dae
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/sequence/SequenceScreenshot.java
@@ -0,0 +1,67 @@
+package plugins.adufour.blocks.tools.sequence;
+
+import icy.canvas.Canvas2D;
+import icy.gui.viewer.Viewer;
+import icy.plugin.abstract_.Plugin;
+import icy.sequence.Sequence;
+import icy.system.thread.ThreadUtil;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarSequence;
+
+public class SequenceScreenshot extends Plugin implements SequenceBlock
+{
+    private VarSequence seq = new VarSequence("Sequence", null);
+
+    private VarSequence seqOut = new VarSequence("Screenshot", null);
+
+    @Override
+    public void run()
+    {
+        final Sequence in = seq.getValue();
+        final int time = in.getSizeT();
+        final int depth = in.getSizeZ();
+        final Sequence out = new Sequence("Screenshot of " + in.getName());
+
+        final Viewer viewer = new Viewer(in, false);
+        final Canvas2D[] canvas2DP = new Canvas2D[1];
+
+        // init
+        canvas2DP[0] = null;
+        // force completion of SWING EDT tasks
+        ThreadUtil.invokeLater(new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                canvas2DP[0] = (Canvas2D) viewer.getCanvas();
+            }
+        });
+
+        final long startTime = System.currentTimeMillis();
+        // wait 2 seconds max for initialization
+        while ((canvas2DP[0] == null) && ((System.currentTimeMillis() - startTime) < 2000L))
+            ThreadUtil.sleep(10);
+
+        final Canvas2D canvas2D = canvas2DP[0];
+
+        for (int t = 0; t < time; t++)
+            for (int z = 0; z < depth; z++)
+                out.setImage(t, z, canvas2D.getRenderedImage(t, z, -1, false));
+
+        viewer.close();
+
+        seqOut.setValue(out);
+    }
+
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input sequence", seq);
+    }
+
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("screenshot", seqOut);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/sequence/ShowSequence.java b/src/main/java/plugins/adufour/blocks/tools/sequence/ShowSequence.java
new file mode 100644
index 0000000000000000000000000000000000000000..8689351ef8056302513412489859e87b7d0fcb15
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/sequence/ShowSequence.java
@@ -0,0 +1,35 @@
+package plugins.adufour.blocks.tools.sequence;
+
+import icy.main.Icy;
+import icy.plugin.abstract_.Plugin;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.vars.lang.VarSequence;
+
+//@BlockSearchAnnotation(
+//	name="show sequence",
+//	category="sequence",
+//	description="",
+//	author="adufour"
+//)
+public class ShowSequence extends Plugin implements SequenceBlock
+{
+    VarSequence sequence = new VarSequence("sequence", null);
+    
+    @Override
+    public void run()
+    {
+        if (sequence.getValue() != null) Icy.getMainInterface().addSequence(sequence.getValue());
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("sequence", sequence);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/tools/text/AppendText.java b/src/main/java/plugins/adufour/blocks/tools/text/AppendText.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd9ed786743c784916f014ba9053fadf0c54406b
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/tools/text/AppendText.java
@@ -0,0 +1,53 @@
+package plugins.adufour.blocks.tools.text;
+
+import icy.plugin.abstract_.Plugin;
+import icy.plugin.interface_.PluginBundled;
+import icy.sequence.Sequence;
+import plugins.adufour.blocks.tools.ToolsBlock;
+import plugins.adufour.blocks.util.VarList;
+import plugins.adufour.protocols.Protocols;
+import plugins.adufour.vars.lang.VarObject;
+import plugins.adufour.vars.lang.VarString;
+
+public class AppendText extends Plugin implements ToolsBlock, PluginBundled
+{
+    VarObject in        = new VarObject("input", null);
+    VarString separator = new VarString("Separator", "_");
+    VarObject suffix    = new VarObject("Suffix", "");
+    
+    VarString out       = new VarString("output", "");
+    
+    @Override
+    public void run()
+    {
+        Object i = in.getValue();
+        
+        Object o = suffix.getValue();
+        
+        String prefix = i == null ? "" : i.toString();
+        
+        if (i instanceof Sequence) prefix = ((Sequence) i).getName();
+        
+        out.setValue(prefix + separator.getValue() + (o == null ? "" : o));
+    }
+    
+    @Override
+    public void declareInput(VarList inputMap)
+    {
+        inputMap.add("input", in);
+        inputMap.add("Separator", separator);
+        inputMap.add("Suffix", suffix);
+    }
+    
+    @Override
+    public void declareOutput(VarList outputMap)
+    {
+        outputMap.add("output", out);
+    }
+    
+    @Override
+    public String getMainPluginClassName()
+    {
+        return Protocols.class.getName();
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/BlockAnnotations.java b/src/main/java/plugins/adufour/blocks/util/BlockAnnotations.java
new file mode 100644
index 0000000000000000000000000000000000000000..05c98c863e645cf4bd0d00bc472499e994f5da4c
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/BlockAnnotations.java
@@ -0,0 +1,232 @@
+package plugins.adufour.blocks.util;
+
+import icy.plugin.abstract_.Plugin;
+import icy.sequence.Sequence;
+
+import java.io.File;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import plugins.adufour.blocks.lang.Block;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarBoolean;
+import plugins.adufour.vars.lang.VarDouble;
+import plugins.adufour.vars.lang.VarFile;
+import plugins.adufour.vars.lang.VarFloat;
+import plugins.adufour.vars.lang.VarInteger;
+import plugins.adufour.vars.lang.VarSequence;
+import plugins.adufour.vars.lang.VarString;
+
+/**
+ * Utility class that parses block annotations and generates blocks from valid methods
+ * 
+ * @author Alexandre Dufour
+ * @see BlockInput
+ * @see BlockMethod
+ */
+public class BlockAnnotations
+{
+    /**
+     * Annotation used to mark a method as block-compatible
+     * 
+     * @author Alexandre Dufour
+     * @see BlockAnnotations
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    @interface BlockMethod
+    {
+        /**
+         * @return The method's description
+         */
+        String value();
+    }
+    
+    /**
+     * Annotation used to mark a method parameter as a block input parameter
+     * 
+     * @author Alexandre Dufour
+     * @see BlockAnnotations
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.PARAMETER)
+    @interface BlockInput
+    {
+        String value();
+    }
+    
+    public static Class<?> getInnerType(Class<? extends Var<?>> variableClass)
+    {
+        if (variableClass == VarInteger.class) return Integer.class;
+        
+        if (variableClass == VarDouble.class) return Double.class;
+        
+        if (variableClass == VarFloat.class) return Float.class;
+        
+        if (variableClass == VarBoolean.class) return Boolean.class;
+        
+        if (variableClass == VarFile.class) return File.class;
+        
+        if (variableClass == VarSequence.class) return Sequence.class;
+        
+        if (variableClass == VarString.class) return String.class;
+        
+        throw new UnsupportedOperationException("Unsupported type: " + variableClass.getSimpleName());
+    }
+    
+    public static Class<? extends Var<?>> getVariableType(Class<?> clazz)
+    {
+        if (clazz == Integer.class) return VarInteger.class;
+        
+        if (clazz == Double.class) return VarDouble.class;
+        
+        if (clazz == Float.class) return VarFloat.class;
+        
+        if (clazz == Boolean.class) return VarBoolean.class;
+        
+        if (clazz == File.class) return VarFile.class;
+        
+        if (clazz == Sequence.class) return VarSequence.class;
+        
+        if (clazz == String.class) return VarString.class;
+        
+        throw new UnsupportedOperationException("Unsupported type: " + clazz.getSimpleName());
+    }
+    
+    public static ArrayList<BlockAnnotations> findBlockMethods(Class<? extends Plugin> clazz)
+    {
+        final ArrayList<BlockAnnotations> blockDescriptors = new ArrayList<BlockAnnotations>();
+        
+        Method[] methods = clazz.getDeclaredMethods();
+        
+        for (final Method method : methods)
+        {
+            // make sure the method is static and annotated
+            
+            if (!Modifier.isStatic(method.getModifiers())) continue;
+            
+            BlockMethod blockFunction = method.getAnnotation(BlockMethod.class);
+            if (blockFunction == null) continue;
+            
+            blockDescriptors.add(new BlockAnnotations(method));
+        }
+        
+        return blockDescriptors;
+    }
+    
+    private final Method method;
+    
+    public BlockAnnotations(Method method)
+    {
+        this.method = method;
+    }
+    
+    public String getDescription()
+    {
+        return method.getAnnotation(BlockMethod.class).value();
+    }
+    
+    public Block createBlock()
+    {
+        return new Block()
+        {
+            @SuppressWarnings("rawtypes")
+            private Var outputVariable;
+            
+            private VarList inputMap;
+            
+            @SuppressWarnings("unchecked")
+            @Override
+            public void run()
+            {
+                try
+                {
+                    Object[] arguments = new Object[inputMap.size()];
+                    Iterator<Var<?>> inputIterator = inputMap.iterator();
+                    
+                    for (int i = 0; i < arguments.length; i++)
+                        arguments[i] = inputIterator.next().getValue();
+                    
+                    Object result = method.invoke(null, arguments);
+                    
+                    if (outputVariable != null) outputVariable.setValue(result);
+                }
+                catch (Exception e)
+                {
+                    e.printStackTrace();
+                }
+            }
+            
+            @Override
+            public void declareOutput(VarList outputMap)
+            {
+                try
+                {
+                    Class<?> returnType = method.getReturnType();
+                    if (returnType == Void.TYPE)
+                    {
+                        outputVariable = null;
+                    }
+                    else
+                    {
+                        Class<? extends Var<?>> outputVarClass = getVariableType(returnType);
+                        Constructor<? extends Var<?>> outputConstructor = outputVarClass.getDeclaredConstructor(String.class, returnType);
+                        outputVariable = outputConstructor.newInstance("output (" + returnType.getSimpleName() + ")", null);
+                    }
+                }
+                catch (Exception e)
+                {
+                    e.printStackTrace();
+                }
+                
+                if (outputVariable != null) outputMap.add("output", outputVariable);
+            }
+            
+            @Override
+            public void declareInput(VarList theInputMap)
+            {
+                this.inputMap = theInputMap;
+                
+                try
+                {
+                    Class<?>[] types = method.getParameterTypes();
+                    Annotation[][] annotations = method.getParameterAnnotations();
+                    
+                    for (int i = 0; i < types.length; i++)
+                    {
+                        Class<?> parameterClass = types[i];
+                        String parameterName = parameterClass.getSimpleName();
+                        
+                        for (Annotation annotation : annotations[i])
+                            if (annotation.annotationType() == BlockInput.class)
+                            {
+                                parameterName = ((BlockInput) annotation).value();
+                            }
+                        
+                        Class<? extends Var<?>> inputVarClass = getVariableType(parameterClass);
+                        Constructor<? extends Var<?>> inputConstructor = inputVarClass.getDeclaredConstructor(String.class, parameterClass);
+                        Var<?> instance = inputConstructor.newInstance(parameterName, null);
+                        theInputMap.add(instance.getName(), instance);
+                    }
+                }
+                catch (Exception e)
+                {
+                    e.printStackTrace();
+                }
+            }
+        };
+    }
+    
+    public String getName()
+    {
+        return method.getName();
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/BlockInfo.java b/src/main/java/plugins/adufour/blocks/util/BlockInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..6614cc51572559a5b3dd3ece3b76ffab39c4052e
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/BlockInfo.java
@@ -0,0 +1,23 @@
+package plugins.adufour.blocks.util;
+
+/**
+ * @deprecated Using this interface is no longer recommended, as the information provided here can
+ *             not be parsed either online or via the plug-in installer. Block information (name and
+ *             description) will soon appear on the Icy web site (and the plug-in loader).
+ * @author Alexandre Dufour
+ * 
+ */
+@Deprecated
+public interface BlockInfo
+{
+    /**
+     * @return The title of the block (displays on the graphical interface)
+     */
+    String getName();
+    
+    /**
+     * @return A short description of the block (will appear as a tool tip text when hovering on the
+     *         block title)
+     */
+    String getDescription();
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/BlockListener.java b/src/main/java/plugins/adufour/blocks/util/BlockListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..5c8085ef8c9b99d441d71fe894e58382c60664a2
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/BlockListener.java
@@ -0,0 +1,20 @@
+package plugins.adufour.blocks.util;
+
+import plugins.adufour.blocks.lang.BlockDescriptor;
+import plugins.adufour.blocks.lang.BlockDescriptor.BlockStatus;
+import plugins.adufour.vars.lang.Var;
+
+public interface BlockListener
+{
+    void blockCollapsed(BlockDescriptor block, boolean collapsed);
+    
+    void blockDimensionChanged(BlockDescriptor block, int newWidth, int newHeight);
+    
+    void blockLocationChanged(BlockDescriptor block, int newX, int newY);
+
+    void blockStatusChanged(BlockDescriptor block, BlockStatus status);
+    
+    <T> void blockVariableChanged(BlockDescriptor block, Var<T> variable, T newValue);
+    
+    void blockVariableAdded(BlockDescriptor block, Var<?> variable);
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/BlocksException.java b/src/main/java/plugins/adufour/blocks/util/BlocksException.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c45f688b5eb2560e26c79677352ce3366e7cc74
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/BlocksException.java
@@ -0,0 +1,29 @@
+package plugins.adufour.blocks.util;
+
+/**
+ * Convenience class for the Blocks framework that allows optimizing bug finding / reporting /
+ * fixing.
+ * 
+ * @author Alexandre Dufour
+ */
+public class BlocksException extends RuntimeException
+{
+    private static final long serialVersionUID = 666L;
+    
+    public final boolean      catchException;
+    
+    /**
+     * Creates a new exception with the specified message and catching behavior.
+     * 
+     * @param message
+     *            the error message bound the current exception
+     * @param catchException
+     *            true if the exception should be caught within the EzPlug layer, false to let the
+     *            exception pass to the global exception manager
+     */
+    public BlocksException(String message, boolean catchException)
+    {
+        super(message);
+        this.catchException = catchException;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/BlocksFinder.java b/src/main/java/plugins/adufour/blocks/util/BlocksFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..73c13acaa9fb32558247286a2cc7719540bfb7f2
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/BlocksFinder.java
@@ -0,0 +1,752 @@
+package plugins.adufour.blocks.util;
+
+import icy.gui.menu.PluginMenuItem;
+import icy.image.ImageUtil;
+import icy.plugin.PluginDescriptor;
+import icy.plugin.PluginLoader;
+import icy.plugin.abstract_.Plugin;
+import icy.resource.ResourceUtil;
+import icy.resource.icon.IcyIcon;
+import icy.system.SystemUtil;
+import icy.util.ClassUtil;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Point;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DragGestureEvent;
+import java.awt.dnd.DragGestureListener;
+import java.awt.dnd.DragSource;
+import java.awt.dnd.DragSourceDragEvent;
+import java.awt.dnd.DragSourceDropEvent;
+import java.awt.dnd.DragSourceEvent;
+import java.awt.dnd.DragSourceListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.JComponent;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.TransferHandler;
+
+import plugins.adufour.blocks.lang.Block;
+import plugins.adufour.blocks.lang.BlockDescriptor;
+import plugins.adufour.blocks.lang.Loop;
+import plugins.adufour.blocks.lang.WorkFlow;
+import plugins.adufour.blocks.tools.Display;
+import plugins.adufour.blocks.tools.ReLoop;
+import plugins.adufour.blocks.tools.ToolsBlock;
+import plugins.adufour.blocks.tools.ij.IJBlock;
+import plugins.adufour.blocks.tools.input.InputBlock;
+import plugins.adufour.blocks.tools.io.IOBlock;
+import plugins.adufour.blocks.tools.roi.ROIBlock;
+import plugins.adufour.blocks.tools.sequence.SequenceBlock;
+import plugins.adufour.protocols.gui.BlockSearchPanel;
+import plugins.adufour.protocols.gui.block.WorkFlowContainer;
+
+public class BlocksFinder
+{
+    /**
+     * Indicates whether annotated methods should be parsed and made available as
+     * blocks
+     */
+    public final boolean parseAnnotations = false;
+
+    /**
+     * class for handling Drag and Drop on the menu items
+     * 
+     * @author Ludovic Laborde, Alexandre Dufour
+     */
+    @SuppressWarnings("serial")
+    public class DND_MenuItem extends PluginMenuItem
+            implements MouseListener, Transferable, DragSourceListener, DragGestureListener
+    {
+        private DragSource source;
+        private TransferHandler handler;
+        private PluginDescriptor descriptor;
+
+        private List<MenuItemListener> menuItemListeners;
+
+        public DND_MenuItem(final PluginDescriptor d)
+        {
+            super(d);
+            setOpaque(false);
+
+            for (MouseListener listener : getMouseListeners())
+                removeMouseListener(listener);
+
+            descriptor = d;
+            menuItemListeners = new ArrayList<MenuItemListener>();
+
+            handler = new TransferHandler()
+            {
+                public Transferable createTransferable(JComponent c)
+                {
+                    return new DND_MenuItem(descriptor);
+                }
+            };
+
+            source = new DragSource();
+            source.createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY, this);
+
+            setTransferHandler(handler);
+            addMouseListener(this);
+        }
+
+        public PluginDescriptor getDescriptor()
+        {
+            return descriptor;
+        }
+
+        @Override
+        public void dragGestureRecognized(DragGestureEvent dge)
+        {
+            source.startDrag(dge, DragSource.DefaultMoveDrop, new DND_MenuItem(descriptor), this);
+        }
+
+        @Override
+        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
+        {
+            return this;
+        }
+
+        @Override
+        public DataFlavor[] getTransferDataFlavors()
+        {
+            try
+            {
+                return new DataFlavor[] {
+                        new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=java.util.ArrayList")};
+            }
+            catch (ClassNotFoundException e)
+            {
+                e.printStackTrace();
+            }
+            return null;
+        }
+
+        @Override
+        public boolean isDataFlavorSupported(DataFlavor flavor)
+        {
+            return true;
+        }
+
+        @Override
+        public void mouseClicked(MouseEvent arg0)
+        {
+            setBackground(Color.GRAY);
+            notifyListeners();
+        }
+
+        public void addMenuItemListener(MenuItemListener l)
+        {
+            menuItemListeners.add(l);
+        }
+
+        // updates the search panel
+        private void notifyListeners()
+        {
+            for (MenuItemListener l : menuItemListeners)
+                l.displayDoc(descriptor);
+        }
+
+        @Override
+        public void dragDropEnd(DragSourceDropEvent dsde)
+        {
+            setBackground(Color.GRAY);
+            notifyListeners();
+        }
+
+        @Override
+        public void dragEnter(DragSourceDragEvent dsde)
+        {
+        }
+
+        @Override
+        public void dragExit(DragSourceEvent dse)
+        {
+        }
+
+        @Override
+        public void dragOver(DragSourceDragEvent dsde)
+        {
+        }
+
+        @Override
+        public void dropActionChanged(DragSourceDragEvent dsde)
+        {
+        }
+
+        @Override
+        public void mouseEntered(MouseEvent arg0)
+        {
+        }
+
+        @Override
+        public void mouseExited(MouseEvent arg0)
+        {
+        }
+
+        @Override
+        public void mousePressed(MouseEvent arg0)
+        {
+        }
+
+        @Override
+        public void mouseReleased(MouseEvent arg0)
+        {
+        }
+    }
+
+    /**
+     * Searches for a block using the specified search text, and populates the
+     * specified search panel with the search results
+     * 
+     * @param menuContainer BlockSearchPanel
+     * @param searchText String
+     */
+    public void createSearchMenu(BlockSearchPanel menuContainer, String searchText)
+    {
+        ArrayList<PluginDescriptor> plugins = PluginLoader.getPlugins(Block.class, true, false, false);
+
+        if (plugins.size() == 0)
+        {
+            JMenuItem item = new JMenuItem(
+                    "The plug-in list has been updated.\nPlease close and re-open the Protocols editor.");
+            item.setFont(item.getFont().deriveFont(Font.ITALIC));
+            menuContainer.add(item);
+            return;
+        }
+
+        for (PluginDescriptor descriptor : plugins)
+        {
+
+            Class<? extends Plugin> _class = descriptor.getPluginClass();
+            final Class<? extends Block> blockClass = _class.asSubclass(Block.class);
+
+            if (ClassUtil.isAbstract(blockClass))
+                continue;
+            if (ClassUtil.isPrivate(blockClass))
+                continue;
+
+            // ALEX (2014-06-17): removing annotations for now
+            // // handle default annotated blocks defined in protocols
+            // if (blockClass.isAnnotationPresent(BlockSearchAnnotation.class))
+            // {
+            // Annotation annotation =
+            // blockClass.getAnnotation(BlockSearchAnnotation.class);
+            // BlockSearchAnnotation blockSearchAnnotation = (BlockSearchAnnotation)
+            // annotation;
+            //
+            // // search conditions
+            // if
+            // (!(blockSearchAnnotation.name().toLowerCase().contains(searchText.toLowerCase())
+            // ||
+            // blockSearchAnnotation.category().toLowerCase().contains(searchText.toLowerCase())
+            // || blockSearchAnnotation.description().toLowerCase()
+            // .contains(searchText.toLowerCase()))) continue;
+            // }
+
+            // handle other external plugins
+            // search conditions
+
+            String testString = "block";
+            testString += descriptor.getName();
+            testString += blockClass.getSimpleName();
+            testString += descriptor.getDescription();
+            testString += descriptor.getAuthor();
+            testString = testString.toLowerCase();
+
+            searchText = searchText.trim().toLowerCase();
+            String searchTextNoSpaces = searchText.replace(" ", "");
+
+            if (testString.contains(searchText) || testString.contains(searchTextNoSpaces))
+            {
+                DND_MenuItem menuItem = new DND_MenuItem(descriptor);
+                menuItem.addMenuItemListener(menuContainer);
+                menuItem.setMaximumSize(new Dimension(Integer.MAX_VALUE, 35));
+                menuItem.setAlignmentX(Component.LEFT_ALIGNMENT);
+
+                String name = blockClass.getSimpleName();
+                name = descriptor.getName().equalsIgnoreCase(name) ? getFlattened(name) : descriptor.getName();
+
+                // Remove the "block" word at the end of the class name
+                if (name.toLowerCase().endsWith("block"))
+                    name = name.substring(0, name.length() - 5);
+
+                if (blockClass.isAnnotationPresent(Deprecated.class))
+                {
+                    name = "(deprecated) " + name;
+                }
+                menuItem.setText(name);
+                menuContainer.add(menuItem);
+            }
+        }
+    }
+
+    /**
+     * Create a JMenu component with all existing classes implementing {@link Block}
+     *
+     * @param menuContainer Container
+     * @param workFlowPane WorkFlowContainer
+     * @param location Point
+     */
+    public final void createJMenu(Container menuContainer, final WorkFlowContainer workFlowPane, final Point location)
+    {
+        JMenu mnBlocks = new JMenu("Blocks...");
+        mnBlocks.setIcon(new IcyIcon(ResourceUtil.getAlphaIconAsImage("box.png"), 22));
+
+        JMenu mnInput = new JMenu("Read...");
+        mnInput.setIcon(new IcyIcon(ResourceUtil.ICON_REDO, 22));
+
+        JMenu mnIO = new JMenu("I/O...");
+        mnIO.setIcon(new IcyIcon(ResourceUtil.ICON_SAVE, 22));
+
+        JMenu mnSeq = new JMenu("Sequence...");
+        mnSeq.setIcon(new IcyIcon(ResourceUtil.ICON_PHOTO, 22));
+
+        JMenu mnROI = new JMenu("ROI...");
+        mnROI.setIcon(new IcyIcon(ResourceUtil.ICON_ROI_POLYGON, 22));
+
+        JMenu mnImageJ = new JMenu("ImageJ...");
+        mnImageJ.setIcon(new IcyIcon(ResourceUtil.ICON_TOIJ, 22));
+
+        JMenu mnLoops = new JMenu("Loop / Batch...");
+        mnLoops.setIcon(new IcyIcon(ResourceUtil.ICON_RELOAD, 22));
+
+        JMenu mnTools = new JMenu("Tools...");
+        mnTools.setIcon(new IcyIcon(ResourceUtil.ICON_TOOLS, 22));
+
+        ArrayList<PluginDescriptor> plugins = PluginLoader.getPlugins(Block.class, true, false, false);
+
+        if (plugins.size() == 0)
+        {
+            JMenuItem item = new JMenuItem(
+                    "The plug-in list has been updated.\nPlease close and re-open the Protocols editor.");
+            item.setFont(item.getFont().deriveFont(Font.ITALIC));
+            menuContainer.add(item);
+            return;
+        }
+
+        for (PluginDescriptor descriptor : plugins)
+        {
+            Class<? extends Plugin> clazz = descriptor.getPluginClass();
+            try
+            {
+                final Class<? extends Block> blockClass = clazz.asSubclass(Block.class);
+
+                if (ClassUtil.isAbstract(blockClass))
+                    continue;
+                if (ClassUtil.isPrivate(blockClass))
+                    continue;
+                if (blockClass.isAnnotationPresent(Deprecated.class))
+                    continue;
+
+                // create the menu item
+                PluginMenuItem menuItem = new PluginMenuItem(descriptor);
+
+                // remove the internal action listener
+                menuItem.removeActionListener(menuItem);
+
+                String name = blockClass.getSimpleName();
+
+                name = descriptor.getName().equalsIgnoreCase(name) ? getFlattened(name) : descriptor.getName();
+
+                // Remove the "block" word at the end of the class name
+                if (name.toLowerCase().endsWith("block"))
+                    name = name.substring(0, name.length() - 5);
+
+                menuItem.setText(name);
+
+                if (!descriptor.getDescription().isEmpty())
+                {
+                    menuItem.setToolTipText(
+                            "<html>" + descriptor.getDescription().replaceAll("\n", "<br/>") + "</html>");
+                }
+
+                menuItem.addActionListener(new ActionListener()
+                {
+                    @Override
+                    public void actionPerformed(ActionEvent arg0)
+                    {
+                        try
+                        {
+                            addBlock(workFlowPane, blockClass.newInstance(), location);
+                        }
+                        catch (InstantiationException e1)
+                        {
+                            e1.printStackTrace();
+                        }
+                        catch (IllegalAccessException e1)
+                        {
+                            e1.printStackTrace();
+                        }
+                    }
+                });
+
+                // place the menu item where appropriate
+
+                if (blockClass == Display.class)
+                {
+                    // will be dealt with outside this loop
+                    continue;
+                }
+                else if (ClassUtil.isSubClass(blockClass, Loop.class)
+                        && !blockClass.isAnnotationPresent(Deprecated.class))
+                {
+                    final JMenuItem item = new JMenuItem(getFlattened(blockClass.getSimpleName()));
+                    item.addActionListener(new ActionListener()
+                    {
+                        @Override
+                        public void actionPerformed(ActionEvent arg0)
+                        {
+                            try
+                            {
+                                addBlock(workFlowPane, blockClass.newInstance(), location);
+                            }
+                            catch (InstantiationException e1)
+                            {
+                                e1.printStackTrace();
+                            }
+                            catch (IllegalAccessException e1)
+                            {
+                                e1.printStackTrace();
+                            }
+                        }
+                    });
+                    mnLoops.add(item);
+                }
+                else if (InputBlock.class.isAssignableFrom(blockClass))
+                {
+                    mnInput.add(menuItem);
+                }
+                else if (SequenceBlock.class.isAssignableFrom(blockClass))
+                {
+                    mnSeq.add(menuItem);
+                }
+                else if (IOBlock.class.isAssignableFrom(blockClass))
+                {
+                    mnIO.add(menuItem);
+                }
+                else if (ROIBlock.class.isAssignableFrom(blockClass))
+                {
+                    mnROI.add(menuItem);
+                }
+                else if (IJBlock.class.isAssignableFrom(blockClass))
+                {
+                    mnImageJ.add(menuItem);
+                }
+                else if (ToolsBlock.class.isAssignableFrom(blockClass))
+                {
+                    if (blockClass == ReLoop.class)
+                        continue; // TODO
+
+                    mnTools.add(menuItem);
+                }
+                else
+                {
+                    // default case: put the rest in the "Blocks" menu
+                    mnBlocks.add(menuItem);
+                }
+
+                if (parseAnnotations)
+                {
+                    // find annotated methods
+                    ArrayList<BlockAnnotations> blocks = BlockAnnotations.findBlockMethods(clazz);
+
+                    for (final BlockAnnotations annotatedMethod : blocks)
+                    {
+                        JMenuItem item2 = new JMenuItem(clazz.getSimpleName() + "." + annotatedMethod.getName());
+                        item2.setToolTipText(annotatedMethod.getDescription());
+                        item2.addActionListener(new ActionListener()
+                        {
+                            @Override
+                            public void actionPerformed(ActionEvent e)
+                            {
+                                addBlock(workFlowPane, annotatedMethod.createBlock(), location);
+                            }
+                        });
+                        mnBlocks.add(item2);
+                    }
+                }
+            }
+            catch (ClassCastException e1)
+            {
+            }
+        }
+
+        splitLongMenus(mnBlocks, 15);
+        splitLongMenus(mnInput, 15);
+        splitLongMenus(mnSeq, 15);
+        splitLongMenus(mnROI, 15);
+        splitLongMenus(mnIO, 15);
+        splitLongMenus(mnImageJ, 15);
+        splitLongMenus(mnLoops, 15);
+        splitLongMenus(mnTools, 15);
+
+        menuContainer.add(mnBlocks);
+        menuContainer.add(mnInput);
+        menuContainer.add(mnSeq);
+        menuContainer.add(mnROI);
+        menuContainer.add(mnIO);
+        menuContainer.add(mnImageJ);
+        menuContainer.add(mnLoops);
+        menuContainer.add(mnTools);
+
+        // and add the Display block last
+
+        JMenuItem mnDisp = new JMenuItem("Display");
+        mnDisp.addActionListener(new ActionListener()
+        {
+            @Override
+            public void actionPerformed(ActionEvent arg0)
+            {
+                try
+                {
+                    addBlock(workFlowPane, Display.class.newInstance(), location);
+                }
+                catch (InstantiationException e1)
+                {
+                    e1.printStackTrace();
+                }
+                catch (IllegalAccessException e1)
+                {
+                    e1.printStackTrace();
+                }
+            }
+        });
+        mnDisp.setIcon(new IcyIcon(ImageUtil.scaleQuality(ResourceUtil.getAlphaIconAsImage("eye_open.png"), 22, 22)));
+        menuContainer.add(mnDisp);
+
+        // // 3rd-party block providers (very soon...)
+        //
+        // JMenu mnOthers = new JMenu("Others...");
+        // mnOthers.setIcon(new IcyIcon(ResourceUtil.getAlphaIconAsImage("box.png"),
+        // 22));
+        //
+        // for (PluginDescriptor providerPlugin :
+        // PluginLoader.getPlugins(BlockProvider.class, true,
+        // false, false))
+        // {
+        // try
+        // {
+        // BlockProvider provider =
+        // providerPlugin.getPluginClass().asSubclass(BlockProvider.class).newInstance();
+        //
+        // Map<String, Block> blocks = provider.getBlocks();
+        //
+        // for(String blockName : blocks.keySet())
+        // {
+        // final Block block = blocks.get(blockName);
+        // final JMenuItem item = new JMenuItem(blockName);
+        // item.addActionListener(new ActionListener()
+        // {
+        // @Override
+        // public void actionPerformed(ActionEvent arg0)
+        // {
+        // try
+        // {
+        // addBlock(workFlowPane, block.getClass().newInstance(), location);
+        // }
+        // catch (InstantiationException e1)
+        // {
+        // e1.printStackTrace();
+        // }
+        // catch (IllegalAccessException e1)
+        // {
+        // e1.printStackTrace();
+        // }
+        // }
+        // });
+        // mnOthers.add(item);
+        // }
+        // }
+        // catch (Exception e)
+        // {
+        // // TODO Auto-generated catch block
+        // e.printStackTrace();
+        // }
+        // }
+        // splitLongMenus(mnOthers, 15);
+        // menuContainer.add(mnOthers);
+    }
+
+    /**
+     * Create a JMenu component with all existing classes implementing {@link Block}
+     *
+     * @param menuContainer Container
+     * @param workFlowPane WorkFlowContainer
+     */
+    public static void createEmbedJMenu(final Container menuContainer, final WorkFlowContainer workFlowPane)
+    {
+        // One day, when Java 8 rules...
+        // PluginLoader.getPlugins(WorkFlow.class).stream()
+        // // get the plug-in class
+        // .map(PluginDescriptor::getPluginClass)
+        // // Create a menu item for each element
+        // .forEach(enclosure -> {
+        // String menuName = getFlattened(enclosure.getSimpleName());
+        // JMenuItem menuItem = new JMenuItem(menuName);
+        // menuItem.addActionListener(a -> {
+        // try
+        // {
+        // workFlowPane.embedWorkFlow((Class<? extends WorkFlow>) enclosure);
+        // }
+        // catch (Exception e)
+        // {
+        // throw new RuntimeException(e);
+        // }
+        // });
+        // menuContainer.add(menuItem);
+        // });
+
+        // In the meantime...
+        for (PluginDescriptor plugin : PluginLoader.getPlugins(WorkFlow.class))
+        {
+            JMenuItem menuItem = new JMenuItem(getFlattened(plugin.getName()));
+            final PluginDescriptor pf = plugin;
+
+            menuItem.addActionListener(new ActionListener()
+            {
+                @Override
+                public void actionPerformed(ActionEvent arg0)
+                {
+                    try
+                    {
+                        workFlowPane.embedWorkFlow(pf.getPluginClass().asSubclass(WorkFlow.class));
+                    }
+                    catch (Exception e)
+                    {
+                        e.printStackTrace();
+                    }
+                }
+            });
+            menuContainer.add(menuItem);
+        }
+    }
+
+    private static void addBlock(WorkFlowContainer workFlowPane, Block block, Point location)
+    {
+        BlockDescriptor blockDesc = (block instanceof WorkFlow ? ((WorkFlow) block).getBlockDescriptor()
+                : new BlockDescriptor(-1, block));
+
+        blockDesc.setLocation(location.x, location.y);
+
+        workFlowPane.getWorkFlow().addBlock(blockDesc);
+    }
+
+    /**
+     * Breaks the list of items in the specified menu, by creating sub-menus
+     * containing the specified number of items, and a "More..." menu to access
+     * subsequent items.
+     * 
+     * @param menu
+     *        the menu to break into smaller sub-menus
+     * @param maxItemsPerMenu
+     *        the maximum number of items to display in each sub-menu
+     */
+    private void splitLongMenus(JMenu menu, int maxItemsPerMenu)
+    {
+        ArrayList<Component> components = new ArrayList<Component>(Arrays.asList(menu.getPopupMenu().getComponents()));
+
+        if (components.size() > maxItemsPerMenu)
+        {
+            menu.removeAll();
+
+            JMenu currentMenu = menu;
+
+            while (components.size() > 0)
+            {
+                int n = Math.min(components.size(), maxItemsPerMenu - 1);
+
+                for (int i = 0; i < n; i++)
+                    currentMenu.add(components.remove(0));
+
+                if (components.size() > 0)
+                    currentMenu = (JMenu) currentMenu.add(new JMenu("More..."));
+            }
+
+            if (components.size() > 0)
+                throw new RuntimeException("Error while splitting menus: " + components.size() + " are remaining.");
+        }
+
+        // do this recursively for sub-menus
+        for (Component component : menu.getPopupMenu().getComponents())
+        {
+            if (component instanceof JMenu)
+                splitLongMenus((JMenu) component, maxItemsPerMenu);
+        }
+    }
+
+    /**
+     * Creates a flattened version of the provided String. The flattening operation
+     * splits the string by inserting spaces between words starting with an upper
+     * case letter, and converts upper case letters to lower case (with the
+     * exception of the first word). Note that <b>consecutive upper case letters</b>
+     * <b>will remain grouped</b>, as they are considered to represent an acronym.<br>
+     * <br>
+     * <u>NOTE:</u> This method is optimized for class names that follow the Java
+     * naming convention. <br>
+     * Examples:<br>
+     * MyGreatClass -&lt; "My great class"<br>
+     * MyXYZClass -&lt; "My XYZ class"
+     * 
+     * @param string
+     *        the string to flatten
+     * @return a flattened (i.e. pretty-printed) String based on the name of the
+     *         string
+     */
+    public static String getFlattened(String string)
+    {
+        String[] words = string.split("(?=[A-Z])");
+
+        String output = words[0];
+        if (words.length > 1)
+        {
+            int nextWordIndex = 1;
+
+            final int javaVersion = (int) SystemUtil.getJavaVersionAsNumber();
+            if (javaVersion < 8)
+            {
+                output = words[1];
+                nextWordIndex++;
+            }
+
+            for (int i = nextWordIndex; i < words.length; i++)
+            {
+                String word = words[i];
+                if (word.length() == 1)
+                {
+                    // single letter
+                    if (words[i - 1].length() == 1)
+                    {
+                        // append to the previous letter (acronym)
+                        output += word;
+                    }
+                    else
+                    {
+                        // new isolated letter or acronym
+                        output += " " + word;
+                    }
+                }
+                else
+                    output += " " + word.toLowerCase();
+            }
+        }
+
+        return output;
+    }
+
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/BlocksML.java b/src/main/java/plugins/adufour/blocks/util/BlocksML.java
new file mode 100644
index 0000000000000000000000000000000000000000..c77a0acd0b131bd7e5b71443f44d54dbba2e5583
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/BlocksML.java
@@ -0,0 +1,1753 @@
+package plugins.adufour.blocks.util;
+
+import java.awt.Point;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import javax.swing.filechooser.FileFilter;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+
+import icy.gui.frame.progress.AnnounceFrame;
+import icy.main.Icy;
+import icy.network.NetworkUtil;
+import icy.plugin.PluginDescriptor;
+import icy.plugin.PluginInstaller;
+import icy.plugin.PluginLoader;
+import icy.plugin.PluginRepositoryLoader;
+import icy.plugin.PluginUpdater;
+import icy.plugin.interface_.PluginBundled;
+import icy.system.IcyExceptionHandler;
+import icy.system.IcyHandledException;
+import icy.system.thread.ThreadUtil;
+import icy.util.ClassUtil;
+import icy.util.XMLUtil;
+import plugins.adufour.blocks.lang.Block;
+import plugins.adufour.blocks.lang.BlockDescriptor;
+import plugins.adufour.blocks.lang.Link;
+import plugins.adufour.blocks.lang.WorkFlow;
+import plugins.adufour.blocks.tools.input.InputBlock;
+import plugins.adufour.protocols.Protocols;
+import plugins.adufour.vars.gui.model.TypeSelectionModel;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarMutable;
+import plugins.adufour.vars.lang.VarMutableArray;
+import plugins.adufour.vars.lang.VarString;
+import plugins.adufour.vars.util.MutableType;
+import plugins.adufour.vars.util.VarListener;
+
+public class BlocksML
+{
+    private static final String RUNTIME = "runtime";
+
+    private static BlocksML instance = new BlocksML();
+
+    public static BlocksML getInstance()
+    {
+        return instance;
+    }
+
+    public static final int CURRENT_VERSION = 4;
+
+    public static final FileFilter XML_FILE_FILTER = new FileFilter()
+    {
+        @Override
+        public String getDescription()
+        {
+            return "Icy protocols (.xml | .protocol)";
+        }
+
+        @Override
+        public boolean accept(File f)
+        {
+            return f.isDirectory() || f.getPath().toLowerCase().endsWith(".xml")
+                    || f.getPath().toLowerCase().endsWith(".protocol");
+        }
+    };
+
+    private Transformer transformer;
+
+    private VarString status = new VarString("status", "");
+
+    private BlocksML()
+    {
+        try
+        {
+            transformer = TransformerFactory.newInstance().newTransformer();
+            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+            transformer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
+            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        }
+        catch (Exception e)
+        {
+        }
+    }
+
+    public void addStatusListener(VarListener<String> listener)
+    {
+        status.addListener(listener);
+    }
+
+    public void removeStatusListener(VarListener<String> listener)
+    {
+        status.removeListener(listener);
+    }
+
+    /**
+     * Generates the XML representation of this work flow as a String object.
+     * 
+     * @param workFlow
+     *        Target workflow instance.
+     * @return A XML representation of this work flow as a String object
+     * @throws TransformerException
+     *         If an unrecoverable error occurs during the course of the transformation.
+     */
+    public synchronized String toString(WorkFlow workFlow) throws TransformerException
+    {
+        return toString(toXML(workFlow));
+    }
+
+    /**
+     * Converts an XML document to its string representation.
+     * 
+     * @param xml
+     *        The target xml document to transform into string.
+     * @return A XML representation of this work flow as a String object
+     * @throws TransformerException
+     *         If an unrecoverable error occurs during the course of the transformation.
+     */
+    public synchronized String toString(Document xml) throws TransformerException
+    {
+        xml.normalizeDocument();
+
+        DocumentType doctype = xml.getDoctype();
+        DOMSource domSource = new DOMSource(xml);
+        StringWriter string = new StringWriter();
+        StreamResult streamResult = new StreamResult(string);
+
+        if (doctype != null)
+        {
+            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, doctype.getPublicId());
+            transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doctype.getSystemId());
+        }
+
+        transformer.transform(domSource, streamResult);
+
+        return string.toString();
+    }
+
+    /**
+     * Creates a XML document representing the specified work flow.
+     * 
+     * @param workFlow
+     *        The work flow to save.
+     * @return The XML document representing the target workflow.
+     */
+    public synchronized Document toXML(WorkFlow workFlow)
+    {
+        Document xml = XMLUtil.createDocument(false);
+        Element workSpaceRoot = XMLUtil.createRootElement(xml, "protocol");
+
+        XMLUtil.setAttributeIntValue(workSpaceRoot, "VERSION", CURRENT_VERSION);
+
+        switch (CURRENT_VERSION)
+        {
+            case 1:
+                saveWorkFlow_V1(workFlow, workSpaceRoot);
+                break;
+            case 2:
+                saveWorkFlow_V2(workFlow, workSpaceRoot);
+                break;
+            case 3:
+                saveWorkFlow_V3(workFlow, workSpaceRoot);
+                break;
+            case 4:
+                saveWorkFlow_V4(workFlow, workSpaceRoot);
+                break;
+            default:
+                throw new UnsupportedOperationException("Cannot save Blocks ML version " + CURRENT_VERSION);
+        }
+
+        return xml;
+    }
+
+    /**
+     * Saves the specified work flow into the specified file in XML format.
+     * 
+     * @param workFlow
+     *        The work flow to save.
+     * @param f
+     *        The file to write (or overwrite).
+     * @throws BlocksException
+     *         If the file could not be saved.
+     * @throws IOException
+     *         If an error occurs when saving the workflow in the file.
+     */
+    public synchronized void saveWorkFlow(WorkFlow workFlow, File f) throws BlocksException, IOException
+    {
+        Document xml = toXML(workFlow);
+
+        if (!XMLUtil.saveDocument(xml, f))
+            throw new IOException(
+                    "Unable to save the protocol.\nDo you have write permissions to the destination folder.");
+    }
+
+    /**
+     * @deprecated Legacy method, use {@link #saveWorkFlow_V4(WorkFlow, Element)} instead.
+     * @param workFlow
+     *        The workflow to save.
+     * @param workFlowRoot
+     *        Root element where elements are to be inserted.
+     */
+    @SuppressWarnings("deprecation")
+    @Deprecated
+    public synchronized void saveWorkFlow_V1(WorkFlow workFlow, Element workFlowRoot)
+    {
+        Element blocksNode = XMLUtil.addElement(workFlowRoot, "blocks");
+
+        for (BlockDescriptor blockData : workFlow)
+        {
+            Element blockNode;
+            Block block = blockData.getBlock();
+
+            if (block instanceof WorkFlow)
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "workflow");
+            }
+            else
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "block");
+            }
+
+            XMLUtil.setAttributeValue(blockNode, "type", block.getClass().getCanonicalName());
+            XMLUtil.setAttributeIntValue(blockNode, "ID", workFlow.indexOf(blockData));
+            XMLUtil.setAttributeIntValue(blockNode, "xLocation", blockData.getLocation().x);
+            XMLUtil.setAttributeIntValue(blockNode, "yLocation", blockData.getLocation().y);
+
+            if (block instanceof WorkFlow)
+            {
+                WorkFlow innerWorkFlow = (WorkFlow) block;
+
+                saveWorkFlow_V1(innerWorkFlow, blockNode);
+
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    BlockDescriptor owner = innerWorkFlow.getInputOwner(var);
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeIntValue(varNode, "ID", blockData.inputVars.indexOf(var));
+                    XMLUtil.setAttributeIntValue(varNode, "blockID", innerWorkFlow.indexOf(owner));
+                    XMLUtil.setAttributeIntValue(varNode, "varID", owner.inputVars.indexOf(var));
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    BlockDescriptor owner = innerWorkFlow.getOutputOwner(var);
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeIntValue(varNode, "ID", blockData.outputVars.indexOf(var));
+                    XMLUtil.setAttributeIntValue(varNode, "blockID", innerWorkFlow.indexOf(owner));
+                    XMLUtil.setAttributeIntValue(varNode, "varID", owner.outputVars.indexOf(var));
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                }
+            }
+            else
+            {
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeIntValue(varNode, Var.XML_KEY_ID, blockData.inputVars.indexOf(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+                    var.saveToXML(varNode);
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeIntValue(varNode, Var.XML_KEY_ID, blockData.outputVars.indexOf(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+                    var.saveToXML(varNode);
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                }
+            }
+        }
+
+        Element linkRoot = XMLUtil.addElement(workFlowRoot, "links");
+
+        for (Link<?> link : workFlow.getLinksIterator())
+        {
+            Element linkNode = XMLUtil.addElement(linkRoot, "link");
+
+            XMLUtil.setAttributeIntValue(linkNode, "srcBlockID", workFlow.indexOf(link.srcBlock));
+            XMLUtil.setAttributeIntValue(linkNode, "srcVarID", link.srcBlock.outputVars.indexOf(link.srcVar));
+            XMLUtil.setAttributeIntValue(linkNode, "dstBlockID", workFlow.indexOf(link.dstBlock));
+            XMLUtil.setAttributeIntValue(linkNode, "dstVarID", link.dstBlock.inputVars.indexOf(link.dstVar));
+        }
+    }
+
+    /**
+     * @deprecated Legacy method, use {@link #saveWorkFlow_V4(WorkFlow, Element)} instead.
+     * @param workFlow
+     *        The workflow to save.
+     * @param workFlowRoot
+     *        Root element where elements are to be inserted.
+     */
+    @Deprecated
+    public synchronized void saveWorkFlow_V2(WorkFlow workFlow, Element workFlowRoot)
+    {
+        Element blocksNode = XMLUtil.addElement(workFlowRoot, "blocks");
+
+        for (BlockDescriptor blockData : workFlow)
+        {
+            Element blockNode;
+            Block block = blockData.getBlock();
+
+            if (block instanceof WorkFlow)
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "workflow");
+                XMLUtil.setAttributeBooleanValue(blockNode, "collapsed", blockData.isCollapsed());
+            }
+            else
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "block");
+            }
+
+            XMLUtil.setAttributeValue(blockNode, "type", block.getClass().getCanonicalName());
+            XMLUtil.setAttributeIntValue(blockNode, "ID", workFlow.indexOf(blockData));
+            XMLUtil.setAttributeIntValue(blockNode, "xLocation", blockData.getLocation().x);
+            XMLUtil.setAttributeIntValue(blockNode, "yLocation", blockData.getLocation().y);
+
+            if (blockData.isWorkFlow())
+            {
+                WorkFlow innerWorkFlow = (WorkFlow) block;
+
+                saveWorkFlow_V2(innerWorkFlow, blockNode);
+
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    BlockDescriptor owner = innerWorkFlow.getInputOwner(var);
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, "ID", blockData.inputVars.getID(var));
+                    XMLUtil.setAttributeIntValue(varNode, "blockID", innerWorkFlow.indexOf(owner));
+                    XMLUtil.setAttributeValue(varNode, "varID", owner.inputVars.getID(var));
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    BlockDescriptor owner = innerWorkFlow.getOutputOwner(var);
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, "ID", blockData.outputVars.getID(var));
+                    XMLUtil.setAttributeIntValue(varNode, "blockID", innerWorkFlow.indexOf(owner));
+                    XMLUtil.setAttributeValue(varNode, "varID", owner.outputVars.getID(var));
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                }
+            }
+            else
+            {
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, Var.XML_KEY_ID, blockData.inputVars.getID(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+                    var.saveToXML(varNode);
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                    if (var instanceof VarMutable && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getCanonicalName());
+                    }
+                    else if (var instanceof VarMutableArray && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getComponentType().getCanonicalName());
+                    }
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, Var.XML_KEY_ID, blockData.outputVars.getID(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+                    var.saveToXML(varNode);
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                    if (var instanceof VarMutable && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getCanonicalName());
+                    }
+                    else if (var instanceof VarMutableArray && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getComponentType().getCanonicalName());
+                    }
+                }
+            }
+        }
+
+        Element linkRoot = XMLUtil.addElement(workFlowRoot, "links");
+
+        for (Link<?> link : workFlow.getLinksIterator())
+        {
+            Element linkNode = XMLUtil.addElement(linkRoot, "link");
+
+            XMLUtil.setAttributeIntValue(linkNode, "srcBlockID", workFlow.indexOf(link.srcBlock));
+            XMLUtil.setAttributeValue(linkNode, "srcVarID", link.srcBlock.getVarID(link.srcVar));
+            XMLUtil.setAttributeIntValue(linkNode, "dstBlockID", workFlow.indexOf(link.dstBlock));
+            XMLUtil.setAttributeValue(linkNode, "dstVarID", link.dstBlock.getVarID(link.dstVar));
+        }
+    }
+
+    /**
+     * @deprecated Legacy method, use {@link #saveWorkFlow_V4(WorkFlow, Element)} instead.
+     * @param workFlow
+     *        The workflow to save.
+     * @param workFlowRoot
+     *        Root element where elements are to be inserted.
+     */
+    @Deprecated
+    public synchronized void saveWorkFlow_V3(WorkFlow workFlow, Element workFlowRoot)
+    {
+        Element blocksNode = XMLUtil.addElement(workFlowRoot, "blocks");
+
+        for (BlockDescriptor blockData : workFlow)
+        {
+            Element blockNode;
+            Block block = blockData.getBlock();
+
+            if (block instanceof WorkFlow)
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "workflow");
+                XMLUtil.setAttributeBooleanValue(blockNode, "collapsed", blockData.isCollapsed());
+                XMLUtil.setAttributeIntValue(blockNode, "width", blockData.getDimension().width);
+                XMLUtil.setAttributeIntValue(blockNode, "height", blockData.getDimension().height);
+            }
+            else
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "block");
+            }
+
+            String blockClass = block.getClass().getCanonicalName();
+            String mainClass = block instanceof PluginBundled ? ((PluginBundled) block).getMainPluginClassName()
+                    : blockClass;
+
+            XMLUtil.setAttributeValue(blockNode, "className", mainClass);
+            XMLUtil.setAttributeValue(blockNode, "blockType", blockClass);
+            XMLUtil.setAttributeIntValue(blockNode, "ID", workFlow.indexOf(blockData));
+            XMLUtil.setAttributeIntValue(blockNode, "xLocation", blockData.getLocation().x);
+            XMLUtil.setAttributeIntValue(blockNode, "yLocation", blockData.getLocation().y);
+
+            if (blockData.isWorkFlow())
+            {
+                WorkFlow innerWorkFlow = (WorkFlow) block;
+
+                saveWorkFlow_V3(innerWorkFlow, blockNode);
+
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, "ID", innerWorkFlow.getInputVarID(var));
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, "ID", innerWorkFlow.getOutputVarID(var));
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                }
+            }
+            else
+            {
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, Var.XML_KEY_ID, blockData.inputVars.getID(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+                    var.saveToXML(varNode);
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                    if (var instanceof VarMutable && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getCanonicalName());
+                    }
+                    else if (var instanceof VarMutableArray && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getComponentType().getCanonicalName());
+                    }
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, Var.XML_KEY_ID, blockData.outputVars.getID(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+                    var.saveToXML(varNode);
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                    if (var instanceof VarMutable && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getCanonicalName());
+                    }
+                    else if (var instanceof VarMutableArray && var.getType() != null)
+                    {
+                        XMLUtil.setAttributeValue(varNode, "type", var.getType().getComponentType().getCanonicalName());
+                    }
+                }
+            }
+        }
+
+        Element linkRoot = XMLUtil.addElement(workFlowRoot, "links");
+
+        for (Link<?> link : workFlow.getLinksIterator())
+        {
+            Element linkNode = XMLUtil.addElement(linkRoot, "link");
+
+            XMLUtil.setAttributeIntValue(linkNode, "srcBlockID", workFlow.indexOf(link.srcBlock));
+            XMLUtil.setAttributeValue(linkNode, "srcVarID", link.srcBlock.getVarID(link.srcVar));
+            XMLUtil.setAttributeIntValue(linkNode, "dstBlockID", workFlow.indexOf(link.dstBlock));
+            XMLUtil.setAttributeValue(linkNode, "dstVarID", link.dstBlock.getVarID(link.dstVar));
+        }
+    }
+
+    /**
+     * @param workFlow
+     *        The workflow to save.
+     * @param workFlowRoot
+     *        Root element where elements are to be inserted.
+     */
+    public synchronized void saveWorkFlow_V4(WorkFlow workFlow, Element workFlowRoot)
+    {
+        Element blocksNode = XMLUtil.addElement(workFlowRoot, "blocks");
+
+        for (BlockDescriptor blockData : workFlow)
+        {
+            Element blockNode;
+            Block block = blockData.getBlock();
+
+            if (block instanceof WorkFlow)
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "workflow");
+                XMLUtil.setAttributeBooleanValue(blockNode, "collapsed", blockData.isCollapsed());
+            }
+            else
+            {
+                blockNode = XMLUtil.addElement(blocksNode, "block");
+            }
+
+            String blockClass = block.getClass().getName();
+            String mainClass = block instanceof PluginBundled ? ((PluginBundled) block).getMainPluginClassName()
+                    : blockClass;
+
+            XMLUtil.setAttributeValue(blockNode, "className", mainClass);
+            XMLUtil.setAttributeValue(blockNode, "blockType", blockClass);
+            XMLUtil.setAttributeIntValue(blockNode, "ID", blockData.getID());
+            XMLUtil.setAttributeIntValue(blockNode, "xLocation", blockData.getLocation().x);
+            XMLUtil.setAttributeIntValue(blockNode, "yLocation", blockData.getLocation().y);
+            XMLUtil.setAttributeIntValue(blockNode, "width", blockData.getDimension().width);
+            XMLUtil.setAttributeIntValue(blockNode, "height", blockData.getDimension().height);
+            XMLUtil.setAttributeBooleanValue(blockNode, "collapsed", blockData.isCollapsed());
+            XMLUtil.setAttributeValue(blockNode, "definedName", blockData.getDefinedName());
+            XMLUtil.setAttributeBooleanValue(blockNode, "keepsResults", blockData.keepsResults());
+
+            if (block instanceof InputBlock)
+                XMLUtil.setAttributeValue(blockNode, "CommandLineID", blockData.getCommandLineID());
+
+            if (blockData.isWorkFlow())
+            {
+                WorkFlow innerWorkFlow = (WorkFlow) block;
+
+                saveWorkFlow_V4(innerWorkFlow, blockNode);
+
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    BlockDescriptor owner = innerWorkFlow.getInputOwner(var);
+                    if (owner == null)
+                    {
+                        System.err
+                                .println("Warning: could not find owner for input variable: \"" + var + "\", skipping");
+                        continue;
+                    }
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, "ID", blockData.getVarID(var));
+                    XMLUtil.setAttributeIntValue(varNode, "blockID", owner.getID());
+                    var.saveToXML(varNode);
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    BlockDescriptor owner = innerWorkFlow.getOutputOwner(var);
+                    if (owner == null)
+                    {
+                        System.err.println(
+                                "Warning: could not find owner for output variable: \"" + var + "\", skipping");
+                        continue;
+                    }
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, "ID", blockData.getVarID(var));
+                    XMLUtil.setAttributeIntValue(varNode, "blockID", owner.getID());
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                }
+            }
+            else
+            {
+                Element varRoot = XMLUtil.addElement(blockNode, "variables");
+
+                Element inputVarRoot = XMLUtil.addElement(varRoot, "input");
+
+                for (Var<?> var : blockData.inputVars)
+                {
+                    Element varNode = XMLUtil.addElement(inputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, Var.XML_KEY_ID, blockData.getVarID(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+
+                    // only save user values (linked values are stored or generated elsewhere)
+                    if (var.getReference() == null)
+                        var.saveToXML(varNode);
+
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.inputVars.isVisible(var));
+                    if (var instanceof MutableType)
+                    {
+                        if (var instanceof VarMutable && var.getType() != null)
+                        {
+                            XMLUtil.setAttributeValue(varNode, "type", var.getType().getName());
+                        }
+                        else if (var instanceof VarMutableArray && var.getType() != null)
+                        {
+                            XMLUtil.setAttributeValue(varNode, "type", var.getType().getComponentType().getName());
+                        }
+                    }
+                    XMLUtil.setAttributeBooleanValue(varNode, RUNTIME, blockData.inputVars.isRuntimeVariable(var));
+                }
+
+                Element outputVarRoot = XMLUtil.addElement(varRoot, "output");
+
+                for (Var<?> var : blockData.outputVars)
+                {
+                    Element varNode = XMLUtil.addElement(outputVarRoot, "variable");
+                    XMLUtil.setAttributeValue(varNode, Var.XML_KEY_ID, blockData.getVarID(var));
+                    XMLUtil.setAttributeValue(varNode, "name", var.getName());
+                    XMLUtil.setAttributeBooleanValue(varNode, "visible", blockData.outputVars.isVisible(var));
+                    if (var instanceof MutableType)
+                    {
+                        if (var instanceof VarMutable && var.getType() != null)
+                        {
+                            XMLUtil.setAttributeValue(varNode, "type", var.getType().getName());
+                        }
+                        else if (var instanceof VarMutableArray && var.getType() != null)
+                        {
+                            XMLUtil.setAttributeValue(varNode, "type", var.getType().getComponentType().getName());
+                        }
+                    }
+                    XMLUtil.setAttributeBooleanValue(varNode, RUNTIME, blockData.outputVars.isRuntimeVariable(var));
+                }
+            }
+        }
+
+        Element linkRoot = XMLUtil.addElement(workFlowRoot, "links");
+
+        for (Link<?> link : workFlow.getLinksIterator())
+        {
+            Element linkNode = XMLUtil.addElement(linkRoot, "link");
+
+            XMLUtil.setAttributeIntValue(linkNode, "srcBlockID", link.srcBlock.getID());
+            XMLUtil.setAttributeValue(linkNode, "srcVarID", link.srcBlock.getVarID(link.srcVar));
+            if (link.srcVar instanceof MutableType)
+            {
+                if (link.srcVar instanceof VarMutable && link.srcVar.getType() != null)
+                {
+                    XMLUtil.setAttributeValue(linkNode, "srcVarType", link.srcVar.getType().getName());
+                }
+                else if (link.srcVar instanceof VarMutableArray && link.srcVar.getType() != null)
+                {
+                    XMLUtil.setAttributeValue(linkNode, "srcVarType",
+                            link.srcVar.getType().getComponentType().getName());
+                }
+            }
+            XMLUtil.setAttributeIntValue(linkNode, "dstBlockID", link.dstBlock.getID());
+            XMLUtil.setAttributeValue(linkNode, "dstVarID", link.dstBlock.getVarID(link.dstVar));
+        }
+    }
+
+    /**
+     * @param xml
+     *        The xml document to load.
+     * @param targetWorkFlow
+     *        The workflow where elements generated from the xml document are to be inserted.
+     * @return true if the loaded file is at the latest BlocksML version, false otherwise.
+     */
+    public synchronized boolean loadWorkFlow(Document xml, WorkFlow targetWorkFlow)
+    {
+        Element xmlRoot = XMLUtil.getRootElement(xml);
+
+        String rootName = xmlRoot.getNodeName();
+
+        if (!rootName.equalsIgnoreCase("protocol") && !rootName.equalsIgnoreCase("workspace"))
+        {
+            throw new IcyHandledException("The selected file is not an Icy protocol");
+        }
+
+        int fileVersion = XMLUtil.getAttributeIntValue(xmlRoot, "VERSION", -1);
+
+        // scan the protocol for missing plugins
+        Set<String> missingPlugins = getMissingPlugins(xmlRoot, fileVersion);
+
+        if (missingPlugins.size() > 0)
+        {
+            // Are we connected to the internet?
+            if (!NetworkUtil.hasInternetAccess())
+                throw new IcyHandledException(
+                        "Some plugins required by this protocol are missing,  but no internet connection is available");
+
+            // Are we using Icy's plug-in loader (and not Eclipse)?
+            if (getClass().getClassLoader() == ClassLoader.getSystemClassLoader())
+                throw new IcyHandledException(
+                        "Cannot install missing blocks while using Icy4Eclipse in \"Debug\" or \"Run\" modes");
+
+            // we are live!
+            String message = "[Protocols] Installing required plugins...";
+            System.out.println(message);
+            AnnounceFrame announcement = null;
+
+            if (!Icy.getMainInterface().isHeadLess())
+                announcement = new AnnounceFrame(message);
+
+            Set<PluginDescriptor> descriptors = new HashSet<PluginDescriptor>(missingPlugins.size());
+
+            for (String className : missingPlugins)
+            {
+                PluginDescriptor pDesc = PluginRepositoryLoader.getPlugin(className);
+                if (pDesc == null)
+                {
+                    // sadly, I believe the plugin loader could be reloaded at anytime...
+                    // double check that for every plugin, just to be safe
+                    if (PluginLoader.getPlugins(Block.class, true, false, false).size() == 0)
+                        throw new BlocksReloadedException();
+
+                    throw new BlocksException(
+                            "Couldn't find plugin " + ClassUtil.getSimpleClassName(className) + " online", true);
+                }
+                descriptors.add(pDesc);
+            }
+
+            for (PluginDescriptor pDesc : descriptors)
+                PluginInstaller.install(pDesc, false);
+
+            PluginInstaller.waitInstall();
+            PluginLoader.waitWhileLoading();
+
+            System.out.println("[Protocols] Plugins installed successfully.");
+            if (announcement != null)
+                announcement.close();
+
+            // reload the whole mother!
+            throw new BlocksReloadedException();
+        }
+
+        switch (fileVersion)
+        {
+            case 1:
+                loadWorkFlow_V1(xmlRoot, targetWorkFlow);
+                break;
+            case 2:
+                loadWorkFlow_V2(xmlRoot, targetWorkFlow);
+                break;
+            case 3:
+                loadWorkFlow_V3(xmlRoot, targetWorkFlow);
+                break;
+            case 4:
+                loadWorkFlow_V4(xmlRoot, targetWorkFlow);
+                break;
+            default:
+                throw new UnsupportedOperationException("Unknown Protocol version: " + fileVersion);
+        }
+
+        return fileVersion == CURRENT_VERSION;
+    }
+
+    private Set<String> getMissingPlugins(Element workFlowRoot, int fileVersion)
+    {
+        Set<String> missingPlugins = new HashSet<String>();
+
+        Element blocksRoot = XMLUtil.getElement(workFlowRoot, "blocks");
+
+        // we are not in a node that has blocks => stop the recursion
+        if (blocksRoot == null)
+            return missingPlugins;
+
+        ArrayList<Element> blocksNode = XMLUtil.getElements(blocksRoot);
+
+        for (Element blockNode : blocksNode)
+        {
+            // the class name of the block to find
+            String blockTag = (fileVersion <= 2) ? "type" : "blockType";
+            String blockType = XMLUtil.getAttributeValue(blockNode, blockTag, null);
+            // the class name of the plugin providing this block (may be the same)
+            String pluginClassName = XMLUtil.getAttributeValue(blockNode, "className", blockType);
+
+            try
+            {
+                ClassUtil.findClass(blockType);
+            }
+            catch (ClassNotFoundException e)
+            {
+                // the block appears to be missing
+                // => is the corresponding plug-in missing or out-dated?
+                PluginDescriptor mainPlugin = PluginLoader.getPlugin(pluginClassName);
+                if (mainPlugin == null || PluginUpdater.getUpdate(mainPlugin) != null)
+                {
+                    // store the class name of the missing plug-in
+                    missingPlugins.add(pluginClassName);
+                }
+                else
+                {
+                    // The block is definitely missing, stop everything!
+                    IcyExceptionHandler.handleException(mainPlugin, e, false);
+                }
+            }
+            finally
+            {
+                // whether the current block is missing or not,
+                // perhaps it's a work flow and has more blocks within
+                missingPlugins.addAll(getMissingPlugins(blockNode, fileVersion));
+            }
+        }
+
+        return missingPlugins;
+    }
+
+    /**
+     * @deprecated legacy method. Use {@link #loadWorkFlow_V4(Element, WorkFlow)} instead.
+     * @param workFlowRoot
+     *        XML element of the workflow root to be loaded.
+     * @param workFlow
+     *        Target workflow to add elements from document.
+     */
+    @Deprecated
+    @SuppressWarnings({"unchecked", "deprecation"})
+    public synchronized void loadWorkFlow_V1(Element workFlowRoot, WorkFlow workFlow)
+    {
+        Element blocksRoot = XMLUtil.getElement(workFlowRoot, "blocks");
+
+        ArrayList<Element> blocksNode = XMLUtil.getElements(blocksRoot);
+
+        for (Element blockNode : blocksNode)
+        {
+            String className = XMLUtil.getAttributeValue(blockNode, "type", null);
+
+            int xPos = XMLUtil.getAttributeIntValue(blockNode, "xLocation", 0);
+            int yPos = XMLUtil.getAttributeIntValue(blockNode, "yLocation", 0);
+
+            try
+            {
+                Class<? extends Block> blockClass = (Class<? extends Block>) ClassUtil.findClass(className);
+
+                Block block = blockClass.newInstance();
+
+                if (block == null)
+                    throw new BlocksException("Couldn't create block from class " + blockClass.getName(), true);
+
+                if (block instanceof WorkFlow)
+                {
+                    // load the inner work flow
+                    loadWorkFlow_V1(blockNode, (WorkFlow) block);
+
+                    BlockDescriptor blockInfo = workFlow.addBlock(-1, block, new Point(xPos, yPos));
+
+                    // adjust visibility triggers
+                    {
+                        Element varRoot = XMLUtil.getElement(blockNode, "variables");
+
+                        Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+
+                        for (Element varNode : XMLUtil.getElements(inVarRoot))
+                        {
+                            int id = XMLUtil.getAttributeIntValue(varNode, "ID", -1);
+                            Var<?> var = blockInfo.inputVars.get(id);
+                            boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                            blockInfo.inputVars.setVisible(var, visible);
+                        }
+                        Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+
+                        for (Element varNode : XMLUtil.getElements(outVarRoot))
+                        {
+                            int id = XMLUtil.getAttributeIntValue(varNode, "ID", -1);
+                            Var<?> var = blockInfo.outputVars.get(id);
+                            boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                            blockInfo.outputVars.setVisible(var, visible);
+                        }
+                    }
+                }
+                else
+                {
+                    BlockDescriptor blockInfo = workFlow.addBlock(-1, block, new Point(xPos, yPos));
+
+                    Element varRoot = XMLUtil.getElement(blockNode, "variables");
+                    Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+                    for (Element varNode : XMLUtil.getElements(inVarRoot))
+                    {
+                        int id = XMLUtil.getAttributeIntValue(varNode, Var.XML_KEY_ID, -1);
+                        Var<?> var = blockInfo.inputVars.get(id);
+                        var.loadFromXML(varNode);
+                        blockInfo.inputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+                    }
+                    Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+                    for (Element varNode : XMLUtil.getElements(outVarRoot))
+                    {
+                        int id = XMLUtil.getAttributeIntValue(varNode, Var.XML_KEY_ID, -1);
+                        Var<?> var = blockInfo.outputVars.get(id);
+                        var.loadFromXML(varNode);
+                        blockInfo.outputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+                    }
+                }
+            }
+            catch (Exception e1)
+            {
+                throw new BlocksException("Cannot create block (" + e1.getMessage() + ")", true);
+            }
+        }
+
+        Element linkRoot = XMLUtil.getElement(workFlowRoot, "links");
+
+        for (Element linkNode : XMLUtil.getElements(linkRoot))
+        {
+            int srcBlockID = XMLUtil.getAttributeIntValue(linkNode, "srcBlockID", -1);
+            int srcVarID = XMLUtil.getAttributeIntValue(linkNode, "srcVarID", -1);
+            int dstBlockID = XMLUtil.getAttributeIntValue(linkNode, "dstBlockID", -1);
+            int dstVarID = XMLUtil.getAttributeIntValue(linkNode, "dstVarID", -1);
+
+            BlockDescriptor srcBlock = workFlow.getBlock(srcBlockID);
+            @SuppressWarnings("rawtypes")
+            // this is assumed correct
+            Var srcVar = srcBlock.outputVars.get(srcVarID);
+
+            BlockDescriptor dstBlock = workFlow.getBlock(dstBlockID);
+            @SuppressWarnings("rawtypes")
+            // this is assumed correct
+            Var dstVar = dstBlock.inputVars.get(dstVarID);
+
+            workFlow.addLink(srcBlock, srcVar, dstBlock, dstVar);
+        }
+    }
+
+    /**
+     * @deprecated legacy method. Use {@link #loadWorkFlow_V4(Element, WorkFlow)} instead.
+     * @param workFlowRoot
+     *        XML element of the workflow root to be loaded.
+     * @param workFlow
+     *        Target workflow to add elements from document.
+     */
+    @Deprecated
+    @SuppressWarnings({"unchecked", "rawtypes", "deprecation"})
+    public synchronized void loadWorkFlow_V2(Element workFlowRoot, WorkFlow workFlow)
+    {
+        Element blocksRoot = XMLUtil.getElement(workFlowRoot, "blocks");
+
+        ArrayList<Element> blocksNode = XMLUtil.getElements(blocksRoot);
+
+        for (Element blockNode : blocksNode)
+        {
+            String className = XMLUtil.getAttributeValue(blockNode, "type", null);
+
+            int xPos = XMLUtil.getAttributeIntValue(blockNode, "xLocation", -1);
+            int yPos = XMLUtil.getAttributeIntValue(blockNode, "yLocation", -1);
+
+            try
+            {
+                Class<? extends Block> blockClass = (Class<? extends Block>) ClassUtil.findClass(className);
+
+                Block block = blockClass.newInstance();
+
+                if (block == null)
+                    throw new BlocksException("Couldn't create block from class " + blockClass.getName(), true);
+
+                if (block instanceof WorkFlow)
+                {
+                    BlockDescriptor blockInfo = workFlow.addBlock(-1, block, new Point(xPos, yPos));
+
+                    // load the inner work flow
+                    loadWorkFlow_V2(blockNode, (WorkFlow) block);
+
+                    // adjust visibility triggers
+                    {
+                        Element varRoot = XMLUtil.getElement(blockNode, "variables");
+
+                        Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+
+                        for (Element varNode : XMLUtil.getElements(inVarRoot))
+                        {
+                            String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                            Var<?> var = blockInfo.inputVars.get(uid);
+                            boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                            blockInfo.inputVars.setVisible(var, visible);
+                        }
+                        Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+
+                        for (Element varNode : XMLUtil.getElements(outVarRoot))
+                        {
+                            String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                            Var<?> var = blockInfo.outputVars.get(uid);
+                            boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                            blockInfo.outputVars.setVisible(var, visible);
+                        }
+                    }
+                }
+                else
+                {
+                    BlockDescriptor blockInfo = workFlow.addBlock(-1, block, new Point(xPos, yPos));
+
+                    Element varRoot = XMLUtil.getElement(blockNode, "variables");
+                    Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+                    for (Element varNode : XMLUtil.getElements(inVarRoot))
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockInfo.inputVars.get(uid);
+                        var.loadFromXML(varNode);
+                        blockInfo.inputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+
+                        if (var instanceof MutableType)
+                        {
+                            String type = XMLUtil.getAttributeValue(varNode, "type", null);
+                            if (type != null)
+                            {
+                                if (var instanceof VarMutable)
+                                {
+                                    ((MutableType) var).setType(Class.forName(type));
+                                }
+                                else if (var instanceof VarMutableArray)
+                                {
+                                    ((MutableType) var).setType(Class.forName("[L" + type + ";"));
+                                }
+                            }
+                        }
+                    }
+                    Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+                    for (Element varNode : XMLUtil.getElements(outVarRoot))
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockInfo.outputVars.get(uid);
+                        var.loadFromXML(varNode);
+                        blockInfo.outputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+
+                        if (var instanceof MutableType)
+                        {
+                            String type = XMLUtil.getAttributeValue(varNode, "type", null);
+                            if (type != null)
+                            {
+                                if (var instanceof VarMutable)
+                                {
+                                    ((MutableType) var).setType(Class.forName(type));
+                                }
+                                else if (var instanceof VarMutableArray)
+                                {
+                                    ((MutableType) var).setType(Class.forName("[L" + type + ";"));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            catch (Exception e1)
+            {
+                throw new BlocksException("Cannot create block (" + e1.getMessage() + ")", true);
+            }
+        }
+
+        Element linkRoot = XMLUtil.getElement(workFlowRoot, "links");
+
+        for (Element linkNode : XMLUtil.getElements(linkRoot))
+        {
+            int srcBlockID = XMLUtil.getAttributeIntValue(linkNode, "srcBlockID", -1);
+            String srcVarID = XMLUtil.getAttributeValue(linkNode, "srcVarID", null);
+            int dstBlockID = XMLUtil.getAttributeIntValue(linkNode, "dstBlockID", -1);
+            String dstVarID = XMLUtil.getAttributeValue(linkNode, "dstVarID", null);
+
+            BlockDescriptor srcBlock = workFlow.getBlock(srcBlockID);
+            Var srcVar = srcBlock.outputVars.get(srcVarID);
+            if (srcVar == null)
+                srcVar = srcBlock.inputVars.get(srcVarID);
+
+            BlockDescriptor dstBlock = workFlow.getBlock(dstBlockID);
+            Var dstVar = dstBlock.inputVars.get(dstVarID);
+
+            workFlow.addLink(srcBlock, srcVar, dstBlock, dstVar);
+        }
+    }
+
+    /**
+     * @deprecated legacy method. Use {@link #loadWorkFlow_V4(Element, WorkFlow)} instead.
+     * @param workFlowRoot
+     *        XML element of the workflow root to be loaded.
+     * @param workFlow
+     *        Target workflow to add elements from document.
+     */
+    @Deprecated
+    @SuppressWarnings({"unchecked", "rawtypes", "deprecation"})
+    public synchronized void loadWorkFlow_V3(Element workFlowRoot, WorkFlow workFlow)
+    {
+        Element blocksRoot = XMLUtil.getElement(workFlowRoot, "blocks");
+
+        ArrayList<Element> blocksNode = XMLUtil.getElements(blocksRoot);
+
+        for (Element blockNode : blocksNode)
+        {
+            String blockType = XMLUtil.getAttributeValue(blockNode, "blockType", null);
+
+            int xPos = XMLUtil.getAttributeIntValue(blockNode, "xLocation", -1);
+            int yPos = XMLUtil.getAttributeIntValue(blockNode, "yLocation", -1);
+
+            try
+            {
+                Class<? extends Block> blockClass = (Class<? extends Block>) ClassUtil.findClass(blockType);
+
+                Block block = blockClass.newInstance();
+
+                if (block == null)
+                    throw new BlocksException("Couldn't create block from class " + blockClass.getName(), true);
+
+                if (block instanceof WorkFlow)
+                {
+                    int width = XMLUtil.getAttributeIntValue(blockNode, "width", 500);
+                    int height = XMLUtil.getAttributeIntValue(blockNode, "height", 500);
+                    ((WorkFlow) block).getBlockDescriptor().setDimension(width, height);
+
+                    BlockDescriptor blockInfo = workFlow.addBlock(-1, block, new Point(xPos, yPos));
+
+                    // load the inner work flow
+                    loadWorkFlow_V3(blockNode, (WorkFlow) block);
+
+                    // adjust visibility triggers
+                    {
+                        Element varRoot = XMLUtil.getElement(blockNode, "variables");
+
+                        Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+
+                        for (Element varNode : XMLUtil.getElements(inVarRoot))
+                        {
+                            String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                            Var<?> var = blockInfo.inputVars.get(uid);
+                            boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                            blockInfo.inputVars.setVisible(var, visible);
+                        }
+                        Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+
+                        for (Element varNode : XMLUtil.getElements(outVarRoot))
+                        {
+                            String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                            Var<?> var = blockInfo.outputVars.get(uid);
+                            boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                            blockInfo.outputVars.setVisible(var, visible);
+                        }
+                    }
+                }
+                else
+                {
+                    BlockDescriptor blockInfo = workFlow.addBlock(-1, block, new Point(xPos, yPos));
+
+                    Element varRoot = XMLUtil.getElement(blockNode, "variables");
+                    Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+                    for (Element varNode : XMLUtil.getElements(inVarRoot))
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockInfo.inputVars.get(uid);
+                        var.loadFromXML(varNode);
+                        blockInfo.inputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+
+                        if (var instanceof MutableType)
+                        {
+                            String type = XMLUtil.getAttributeValue(varNode, "type", null);
+                            if (type != null)
+                            {
+                                if (var instanceof VarMutable)
+                                {
+                                    ((MutableType) var).setType(Class.forName(type));
+                                }
+                                else if (var instanceof VarMutableArray)
+                                {
+                                    ((MutableType) var).setType(Class.forName("[L" + type + ";"));
+                                }
+                            }
+                        }
+                    }
+                    Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+                    for (Element varNode : XMLUtil.getElements(outVarRoot))
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockInfo.outputVars.get(uid);
+                        var.loadFromXML(varNode);
+                        blockInfo.outputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+
+                        if (var instanceof MutableType)
+                        {
+                            String type = XMLUtil.getAttributeValue(varNode, "type", null);
+                            if (type != null)
+                            {
+                                if (var instanceof VarMutable)
+                                {
+                                    ((MutableType) var).setType(Class.forName(type));
+                                }
+                                else if (var instanceof VarMutableArray)
+                                {
+                                    ((MutableType) var).setType(Class.forName("[L" + type + ";"));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            catch (Exception e1)
+            {
+                throw new BlocksException("Cannot create block (" + e1.getMessage() + ")", true);
+            }
+        }
+
+        Element linkRoot = XMLUtil.getElement(workFlowRoot, "links");
+
+        for (Element linkNode : XMLUtil.getElements(linkRoot))
+        {
+            int srcBlockID = XMLUtil.getAttributeIntValue(linkNode, "srcBlockID", -1);
+            String srcVarID = XMLUtil.getAttributeValue(linkNode, "srcVarID", null);
+            int dstBlockID = XMLUtil.getAttributeIntValue(linkNode, "dstBlockID", -1);
+            String dstVarID = XMLUtil.getAttributeValue(linkNode, "dstVarID", null);
+
+            BlockDescriptor srcBlock = workFlow.getBlock(srcBlockID);
+            Var srcVar = srcBlock.outputVars.get(srcVarID);
+            if (srcVar == null)
+                srcVar = srcBlock.inputVars.get(srcVarID);
+
+            BlockDescriptor dstBlock = workFlow.getBlock(dstBlockID);
+            Var dstVar = dstBlock.inputVars.get(dstVarID);
+
+            workFlow.addLink(srcBlock, srcVar, dstBlock, dstVar);
+        }
+    }
+
+    /**
+     * @param workFlowRoot
+     *        XML element of the workflow root to be loaded.
+     * @param workFlow
+     *        Target workflow to add elements from document.
+     * @throws BlocksException
+     *         If an error occurs while loading the workflow.
+     */
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public synchronized void loadWorkFlow_V4(Element workFlowRoot, final WorkFlow workFlow) throws BlocksException
+    {
+        boolean hasWarnings = false;
+
+        // Prepare a list of blocks to collapse after loading
+        ArrayList<BlockDescriptor> blocksToCollapse = new ArrayList<BlockDescriptor>();
+
+        // 1) Load blocks and inner work flows
+
+        Element blocksRoot = XMLUtil.getElement(workFlowRoot, "blocks");
+
+        ArrayList<Element> blocksNode = XMLUtil.getElements(blocksRoot);
+
+        for (final Element blockNode : blocksNode)
+        {
+            final String blockType = XMLUtil.getAttributeValue(blockNode, "blockType", null);
+
+            Block block = null;
+
+            try
+            {
+                block = ThreadUtil.invokeNow(new Callable<Block>()
+                {
+                    @Override
+                    public Block call() throws Exception
+                    {
+                        return (Block) ClassUtil.findClass(blockType).newInstance();
+                    }
+                });
+            }
+            catch (Exception e)
+            {
+                String message = "Couldn't create block from class \"" + blockType + "\".\n";
+                message += "Reason: " + e.getClass().getName() + " (" + e.getMessage() + ")";
+
+                throw new BlocksException(message, true);
+            }
+
+            int blockID = XMLUtil.getAttributeIntValue(blockNode, "ID", -1);
+
+            final BlockDescriptor blockDescriptor;
+
+            if (block instanceof WorkFlow)
+            {
+                blockDescriptor = ((WorkFlow) block).getBlockDescriptor();
+                blockDescriptor.setID(blockID);
+            }
+            else
+            {
+                blockDescriptor = new BlockDescriptor(blockID, block);
+            }
+
+            int xPos = XMLUtil.getAttributeIntValue(blockNode, "xLocation", -1);
+            int yPos = XMLUtil.getAttributeIntValue(blockNode, "yLocation", -1);
+            blockDescriptor.setLocation(xPos, yPos);
+
+            int width = XMLUtil.getAttributeIntValue(blockNode, "width", 0);
+            int height = XMLUtil.getAttributeIntValue(blockNode, "height", 0);
+
+            // set the block's dimension, but don't collapse just now
+            // => wait for all the drawing to be done
+            blockDescriptor.setDimension(width, height);
+
+            String definedName = XMLUtil.getAttributeValue(blockNode, "definedName", null);
+            blockDescriptor.setDefinedName(definedName);
+
+            blockDescriptor.keepResults(XMLUtil.getAttributeBooleanValue(blockNode, "keepsResults", true));
+
+            if (block instanceof InputBlock)
+                blockDescriptor.setCommandLineID(XMLUtil.getAttributeValue(blockNode, "CommandLineID", ""));
+
+            workFlow.addBlock(blockDescriptor);
+
+            if (block instanceof WorkFlow)
+            {
+                // load the inner work flow
+                loadWorkFlow_V4(blockNode, (WorkFlow) block);
+
+                // adjust visibility triggers
+
+                Element varRoot = XMLUtil.getElement(blockNode, "variables");
+                Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+                final ArrayList<Element> inVarNodes = XMLUtil.getElements(inVarRoot);
+
+                synchronized (blockDescriptor.inputVars)
+                {
+                    for (Element varNode : inVarNodes)
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockDescriptor.inputVars.get(uid);
+
+                        if (var == null)
+                        {
+                            System.err.println(new NoSuchVariableException(blockDescriptor, uid).getMessage());
+                            continue;
+                        }
+
+                        if (var.getReference() == null)
+                        {
+                            var.loadFromXML(varNode);
+
+                            // adjust visibility
+
+                            boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                            try
+                            {
+                                blockDescriptor.inputVars.setVisible(var, visible);
+                            }
+                            catch (NoSuchVariableException e)
+                            {
+                                if (!hasWarnings)
+                                {
+                                    System.err.println("Error(s) while loading protocol:");
+                                    hasWarnings = true;
+                                }
+                                System.err.println(new NoSuchVariableException(blockDescriptor, uid).getMessage());
+                            }
+                        }
+                    }
+                }
+
+                Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+                final ArrayList<Element> outVarNodes = XMLUtil.getElements(outVarRoot);
+
+                synchronized (blockDescriptor.outputVars)
+                {
+                    for (Element varNode : outVarNodes)
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockDescriptor.outputVars.get(uid);
+
+                        // adjust visibility
+
+                        boolean visible = XMLUtil.getAttributeBooleanValue(varNode, "visible", false);
+                        try
+                        {
+                            blockDescriptor.outputVars.setVisible(var, visible);
+                        }
+                        catch (NoSuchVariableException e)
+                        {
+                            if (!hasWarnings)
+                            {
+                                System.err.println("Error(s) while loading protocol:");
+                                hasWarnings = true;
+                            }
+                            System.err.println(new NoSuchVariableException(blockDescriptor, uid).getMessage());
+                            // throw new NoSuchVariableException(blockInfo, uid);
+                        }
+                    }
+                }
+            }
+            else
+            {
+                Element varRoot = XMLUtil.getElement(blockNode, "variables");
+                Element inVarRoot = XMLUtil.getElement(varRoot, "input");
+                final ArrayList<Element> inVarNodes = XMLUtil.getElements(inVarRoot);
+
+                synchronized (blockDescriptor.inputVars)
+                {
+                    for (Element varNode : inVarNodes)
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockDescriptor.inputVars.get(uid);
+
+                        boolean isDynamicVariable = XMLUtil.getAttributeBooleanValue(varNode, RUNTIME, false);
+
+                        if (var == null)
+                        {
+                            if (isDynamicVariable)
+                            {
+                                var = new VarMutable(XMLUtil.getAttributeValue(varNode, "name", "input"), null);
+                                ((VarMutable) var).setDefaultEditorModel(new TypeSelectionModel());
+                                blockDescriptor.inputVars.addRuntimeVariable(uid, (VarMutable) var);
+                            }
+                            else
+                            {
+                                if (!hasWarnings)
+                                {
+                                    System.err.println("Error(s) while loading protocol:");
+                                    hasWarnings = true;
+                                }
+                                System.err.println(new NoSuchVariableException(blockDescriptor, uid).getMessage());
+                                continue;
+                                // throw new NoSuchVariableException(blockInfo, uid);
+                            }
+                        }
+
+                        try
+                        {
+                            if (var instanceof MutableType)
+                            {
+                                Class<?> mutableType = null;
+
+                                String type = XMLUtil.getAttributeValue(varNode, "type", null);
+
+                                if (type != null)
+                                {
+                                    if (var instanceof VarMutable)
+                                    {
+                                        mutableType = getPrimitiveType(type);
+
+                                        if (mutableType == null)
+                                            mutableType = Class.forName(type);
+                                    }
+                                    else if (var instanceof VarMutableArray)
+                                    {
+                                        mutableType = Class.forName("[L" + type + ";");
+                                    }
+
+                                    ((MutableType) var).setType(mutableType);
+                                }
+                            }
+
+                            var.loadFromXML(varNode);
+
+                            // Input blocks can have their values set from command line
+                            if (block instanceof InputBlock)
+                            {
+                                String clID = blockDescriptor.getCommandLineID();
+                                if (!clID.isEmpty())
+                                {
+                                    Map<String, String> map = Protocols.getCommandLineArguments();
+                                    if (map.containsKey(clID))
+                                    {
+                                        // Set the value of the first (and only) block variable
+                                        var.setValueAsString(map.get(clID));
+                                    }
+                                }
+                            }
+                        }
+                        catch (Exception e)
+                        {
+                            String message = "Unable to read input variable \"" + var.getName() + "\" in block \""
+                                    + blockDescriptor.getDefinedName() + "\".\n";
+                            message += "Reason: " + e.getClass().getName() + " (" + e.getMessage() + ")";
+                            throw new BlocksException(message, true);
+                        }
+                        blockDescriptor.inputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+                    }
+                }
+
+                Element outVarRoot = XMLUtil.getElement(varRoot, "output");
+                final ArrayList<Element> outVarNodes = XMLUtil.getElements(outVarRoot);
+
+                synchronized (blockDescriptor.outputVars)
+                {
+                    for (Element varNode : outVarNodes)
+                    {
+                        String uid = XMLUtil.getAttributeValue(varNode, "ID", null);
+                        Var<?> var = blockDescriptor.outputVars.get(uid);
+
+                        boolean isDynamicVariable = XMLUtil.getAttributeBooleanValue(varNode, RUNTIME, false);
+
+                        if (var == null)
+                        {
+                            if (isDynamicVariable)
+                            {
+                                var = new VarMutable(XMLUtil.getAttributeValue(varNode, "name", "output"), null);
+                                ((VarMutable) var).setDefaultEditorModel(new TypeSelectionModel());
+                                blockDescriptor.outputVars.addRuntimeVariable(uid, (VarMutable) var);
+                            }
+                            else
+                            {
+                                if (!hasWarnings)
+                                {
+                                    System.err.println("Error(s) while loading protocol:");
+                                    hasWarnings = true;
+                                }
+                                System.err.println(new NoSuchVariableException(blockDescriptor, uid).getMessage());
+                                continue;
+                                // throw new NoSuchVariableException(blockInfo, uid);
+                            }
+                        }
+
+                        blockDescriptor.outputVars.setVisible(var,
+                                XMLUtil.getAttributeBooleanValue(varNode, "visible", false));
+
+                        if (var instanceof MutableType)
+                            try
+                            {
+                                String type = XMLUtil.getAttributeValue(varNode, "type", null);
+                                if (type != null)
+                                {
+                                    if (var instanceof VarMutable)
+                                    {
+                                        Class<?> mutableType = getPrimitiveType(type);
+                                        ((MutableType) var)
+                                                .setType(mutableType != null ? mutableType : Class.forName(type));
+                                    }
+                                    else if (var instanceof VarMutableArray)
+                                    {
+                                        ((MutableType) var).setType(Class.forName("[L" + type + ";"));
+                                    }
+                                }
+                            }
+                            catch (ClassNotFoundException e)
+                            {
+                                String message = "Unable to read output variable \"" + var.getName() + "\" in block \""
+                                        + blockDescriptor.getDefinedName() + "\".\n";
+                                message += "Reason: " + e.getClass().getName() + " (" + e.getMessage() + ")";
+                                throw new BlocksException(message, true);
+                            }
+                    }
+                }
+            }
+
+            // prepare to collapse the block if necessary
+            if (XMLUtil.getAttributeBooleanValue(blockNode, "collapsed", false))
+                blocksToCollapse.add(blockDescriptor);
+        }
+
+        // 2) Load links
+
+        Element linkRoot = XMLUtil.getElement(workFlowRoot, "links");
+
+        ArrayList<Element> links = XMLUtil.getElements(linkRoot);
+        Collections.sort(links, new Comparator<Element>()
+        {
+            @Override
+            public int compare(Element link1, Element link2)
+            {
+                int srcBlock1 = XMLUtil.getAttributeIntValue(link1, "srcBlockID", -1);
+                int srcBlock2 = XMLUtil.getAttributeIntValue(link2, "srcBlockID", -1);
+
+                int i1 = workFlow.indexOf(workFlow.getBlockByID(srcBlock1));
+                int i2 = workFlow.indexOf(workFlow.getBlockByID(srcBlock2));
+
+                return i1 < i2 ? -1 : i1 > i2 ? 1 : 0;
+            }
+        });
+
+        for (Element linkNode : links)
+        {
+            int srcBlockID = XMLUtil.getAttributeIntValue(linkNode, "srcBlockID", -1);
+            String srcVarID = XMLUtil.getAttributeValue(linkNode, "srcVarID", null);
+            int dstBlockID = XMLUtil.getAttributeIntValue(linkNode, "dstBlockID", -1);
+            String dstVarID = XMLUtil.getAttributeValue(linkNode, "dstVarID", null);
+
+            // load the source variable
+            BlockDescriptor srcBlock = workFlow.getBlockByID(srcBlockID);
+            Var srcVar = srcBlock.outputVars.get(srcVarID);
+            if (srcVar == null)
+                srcVar = srcBlock.inputVars.get(srcVarID);
+
+            if (srcVar == null)
+            {
+                System.err.println("Cannot create a link from variable " + srcVarID + " (from block " + srcBlock
+                        + "). It may have been removed or renamed.");
+                continue;
+            }
+
+            if (srcVar instanceof MutableType)
+            {
+                String type = XMLUtil.getAttributeValue(linkNode, "srcVarType", null);
+                if (type != null)
+                {
+                    try
+                    {
+                        if (srcVar instanceof VarMutable)
+                        {
+                            Class<?> mutableType = getPrimitiveType(type);
+                            ((MutableType) srcVar).setType(mutableType != null ? mutableType : Class.forName(type));
+                        }
+                        else if (srcVar instanceof VarMutableArray)
+                        {
+                            ((MutableType) srcVar).setType(Class.forName("[L" + type + ";"));
+                        }
+                    }
+                    catch (ClassNotFoundException e1)
+                    {
+                        throw new BlocksException("Cannot create block (" + e1.getMessage() + ") => class not found",
+                                true);
+                    }
+                }
+            }
+
+            // load the destination variable
+            BlockDescriptor dstBlock = workFlow.getBlockByID(dstBlockID);
+
+            Var dstVar = dstBlock.inputVars.get(dstVarID);
+
+            if (dstVar == null)
+            {
+                System.err.println("Cannot link to variable " + dstVarID + " (from block " + dstBlock
+                        + "). It may have been removed or renamed.");
+                continue;
+            }
+
+            workFlow.addLink(srcBlock, srcVar, dstBlock, dstVar);
+        }
+
+        // 3) Collapse necessary blocks
+        for (BlockDescriptor block : blocksToCollapse)
+            block.setCollapsed(true);
+
+        if (hasWarnings)
+            System.err.println("--");
+    }
+
+    public synchronized void loadWorkFlow_V5(Element workFlowRoot, WorkFlow workFlow, WorkFlow parentFlow)
+            throws BlocksException
+    {
+        boolean noWarnings = true;
+
+        Element blocksRoot = XMLUtil.getElement(workFlowRoot, "blocks");
+
+        ArrayList<Element> blocksNode = XMLUtil.getElements(blocksRoot);
+
+        for (Element blockNode : blocksNode)
+        {
+            BlockDescriptor blockDesc = new BlockDescriptor();
+            noWarnings &= blockDesc.loadFromXML(blockNode);
+            workFlow.addBlock(blockDesc);
+        }
+
+        Element linkRoot = XMLUtil.getElement(workFlowRoot, "links");
+
+        for (Element linkNode : XMLUtil.getElements(linkRoot))
+        {
+            @SuppressWarnings("rawtypes")
+            Link link = new Link(workFlow);
+            link.loadFromXML(linkNode);
+        }
+
+        if (!noWarnings)
+            System.err.println("--");
+    }
+
+    /**
+     * @param primitiveName
+     *        The string representation of the primitive type.
+     * @return The Java primitive type represented by the given name, or null if the given name does
+     *         not represent a primitive type
+     */
+    public static Class<?> getPrimitiveType(String primitiveName)
+    {
+        if (primitiveName.equals("byte"))
+            return byte.class;
+        if (primitiveName.equals("short"))
+            return short.class;
+        if (primitiveName.equals("int"))
+            return int.class;
+        if (primitiveName.equals("long"))
+            return long.class;
+        if (primitiveName.equals("char"))
+            return char.class;
+        if (primitiveName.equals("float"))
+            return float.class;
+        if (primitiveName.equals("double"))
+            return double.class;
+        if (primitiveName.equals("boolean"))
+            return boolean.class;
+        if (primitiveName.equals("void"))
+            return void.class;
+
+        return null;
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/BlocksReloadedException.java b/src/main/java/plugins/adufour/blocks/util/BlocksReloadedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..70a13c07b3fbff6b303db7ed309403202612e15f
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/BlocksReloadedException.java
@@ -0,0 +1,10 @@
+package plugins.adufour.blocks.util;
+
+@SuppressWarnings("serial")
+public class BlocksReloadedException extends BlocksException
+{
+    public BlocksReloadedException()
+    {
+        super("The plugin list has been updated", true);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/LinkCutException.java b/src/main/java/plugins/adufour/blocks/util/LinkCutException.java
new file mode 100644
index 0000000000000000000000000000000000000000..16dafeaede359c0022b0ab931ed23594f7b80220
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/LinkCutException.java
@@ -0,0 +1,10 @@
+package plugins.adufour.blocks.util;
+
+@SuppressWarnings("serial")
+public class LinkCutException extends BlocksException
+{
+    public LinkCutException(String s)
+    {
+        super(s, true);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/LoopException.java b/src/main/java/plugins/adufour/blocks/util/LoopException.java
new file mode 100644
index 0000000000000000000000000000000000000000..6336d78cb1476ee11d8f359368c8398d933db7f0
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/LoopException.java
@@ -0,0 +1,18 @@
+package plugins.adufour.blocks.util;
+
+import plugins.adufour.blocks.lang.WorkFlow;
+
+/**
+ * Exception raised whenever a loop is detected in an {@link WorkFlow}
+ * 
+ * @author Alexandre Dufour
+ */
+public class LoopException extends BlocksException
+{
+    private static final long serialVersionUID = 1L;
+    
+    public LoopException()
+    {
+        super("Cannot create loops inside a workflow", true);
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/MenuItemListener.java b/src/main/java/plugins/adufour/blocks/util/MenuItemListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..1a51723db974b032ffe15fabf844d4e4da73f9d0
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/MenuItemListener.java
@@ -0,0 +1,7 @@
+package plugins.adufour.blocks.util;
+
+import icy.plugin.PluginDescriptor;
+
+public interface MenuItemListener {
+	public void displayDoc(PluginDescriptor d);
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/NoSuchBlockException.java b/src/main/java/plugins/adufour/blocks/util/NoSuchBlockException.java
new file mode 100644
index 0000000000000000000000000000000000000000..19eab99670c4eba1c9e42f635fbddb2e3b63ec2c
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/NoSuchBlockException.java
@@ -0,0 +1,25 @@
+package plugins.adufour.blocks.util;
+
+import plugins.adufour.blocks.lang.BlockDescriptor;
+import plugins.adufour.blocks.lang.WorkFlow;
+
+public class NoSuchBlockException extends BlocksException
+{
+    private static final long serialVersionUID = 1L;
+    
+    public NoSuchBlockException(BlockDescriptor block)
+    {
+        super("Cannot find block '" + (block == null ? "null" : block.getName()) + "'", false);
+    }
+    
+    public NoSuchBlockException(int ID)
+    {
+        super("Cannot find block with ID '" + ID + "'", false);
+    }
+    
+    public NoSuchBlockException(int ID, WorkFlow container)
+    {
+        super("Cannot find block with ID '" + ID + "' in workflow '" + container.getBlockDescriptor().getName() + "'", false);
+        
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/NoSuchLinkException.java b/src/main/java/plugins/adufour/blocks/util/NoSuchLinkException.java
new file mode 100644
index 0000000000000000000000000000000000000000..68fc7623e9b530b2744c06e16ef5c11451be0add
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/NoSuchLinkException.java
@@ -0,0 +1,12 @@
+package plugins.adufour.blocks.util;
+
+public class NoSuchLinkException extends BlocksException
+{
+    private static final long serialVersionUID = 1L;
+    
+    public NoSuchLinkException(String message)
+    {
+        super(message, false);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/NoSuchVariableException.java b/src/main/java/plugins/adufour/blocks/util/NoSuchVariableException.java
new file mode 100644
index 0000000000000000000000000000000000000000..5b60c7c2721d64746eeba7e0e86de2b423cc5825
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/NoSuchVariableException.java
@@ -0,0 +1,24 @@
+package plugins.adufour.blocks.util;
+
+import plugins.adufour.blocks.lang.BlockDescriptor;
+import plugins.adufour.vars.lang.Var;
+
+public class NoSuchVariableException extends BlocksException
+{
+    private static final long serialVersionUID = 1L;
+    
+    public NoSuchVariableException(String varName)
+    {
+        super("Variable not found: '" + varName + "'", false);
+    }
+    
+    public NoSuchVariableException(BlockDescriptor blockInfo, String varName)
+    {
+        super("Variable '" + varName + "' not found in block '" + blockInfo.getName() + "'. It may have been removed or renamed.", false);
+    }
+    
+    public NoSuchVariableException(Var<?> src)
+    {
+        this(src == null ? null : src.getName());
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/ScopeException.java b/src/main/java/plugins/adufour/blocks/util/ScopeException.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7488a3c5f1cbe42534fcef1534a48e8ef3440ba
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/ScopeException.java
@@ -0,0 +1,13 @@
+package plugins.adufour.blocks.util;
+
+public class ScopeException extends BlocksException
+{
+    private static final long serialVersionUID = 1L;
+    
+    public ScopeException()
+    {
+        super("Two variables located in different loops or workflows cannot be directly linked.\n"
+                + "Expose the inner-most variable(s) by right-clicking on them and select \"expose variable\"", true);
+    }
+    
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/StopException.java b/src/main/java/plugins/adufour/blocks/util/StopException.java
new file mode 100644
index 0000000000000000000000000000000000000000..8bcce2bf604da2aef26e899db06697bcecccb2c4
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/StopException.java
@@ -0,0 +1,10 @@
+package plugins.adufour.blocks.util;
+
+@SuppressWarnings("serial")
+public class StopException extends RuntimeException
+{
+    public StopException()
+    {
+        super("This workflow was interrupted prematurely");
+    }
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/VarList.java b/src/main/java/plugins/adufour/blocks/util/VarList.java
new file mode 100644
index 0000000000000000000000000000000000000000..a484a3b5fcfe8258bc9f69bbd7214b44edcb4348
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/VarList.java
@@ -0,0 +1,334 @@
+package plugins.adufour.blocks.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+import plugins.adufour.blocks.lang.Block;
+import plugins.adufour.vars.lang.Var;
+import plugins.adufour.vars.lang.VarMutable;
+
+/**
+ * Class defining a map of variables using a {@link java.util.HashMap} dictionary
+ * 
+ * @author Alexandre Dufour
+ */
+public class VarList implements Iterable<Var<?>>
+{
+    private final LinkedHashMap<String, Var<?>> varMap = new LinkedHashMap<String, Var<?>>();
+
+    private final HashMap<Var<?>, Boolean> visibilityMap = new HashMap<Var<?>, Boolean>();
+
+    private final HashMap<Var<?>, Boolean> runtimeVariableMap = new HashMap<Var<?>, Boolean>();
+
+    private final ArrayList<VarVisibilityListener> visibilityListeners = new ArrayList<VarVisibilityListener>();
+
+    private final ArrayList<VarListListener> varListListeners = new ArrayList<VarListListener>();
+
+    /**
+     * Adds the specified variable to this variable list. Each variable is given a unique
+     * identifier, which is here the variable's name. Adding another variable with the same name
+     * using this method will throw a {@link IllegalArgumentException}. Instead, use the
+     * {@link #add(String, Var)} method to specify a unique identifier for each variable.
+     * 
+     * @param variable
+     *        The variable to add
+     * @throws IllegalArgumentException
+     *         if the variable already exists, or if a variable with same unique ID exists
+     * @deprecated Changing the name of a variable will also change the default UID, and protocols
+     *             containing an older version of this block (with the old name) will not reload
+     *             properly. Use {@link #add(String, Var)} to specify a unique identifier (and make
+     *             sure this identifier never changes across versions!)<br>
+     *             NB: for the same reason, when migrating to {@link #add(String, Var)}, use the
+     *             current name of the variable as unique identifier
+     */
+    @Deprecated
+    public void add(Var<?> variable)
+    {
+        add(variable.getName(), variable);
+    }
+
+    /**
+     * Adds the given variable to this list with the specified unique ID. If a variable with same
+     * unique ID already exists, an {@link IllegalArgumentException} is thrown.
+     * 
+     * @param uid
+     *        the unique ID of the variable (within this list)
+     * @param variable
+     *        The variable to add
+     * @throws IllegalArgumentException
+     *         if the variable already exists, or if a variable with same unique ID exists
+     */
+    public void add(String uid, Var<?> variable)
+    {
+        add(uid, variable, false);
+    }
+
+    /**
+     * Add a runtime variable to this list (see {@link #isRuntimeVariable(Var)} for more details).
+     * Runtime variables are limited to {@link VarMutable} for reloading purposes. <br>
+     * WARNING: Do *not* add a runtime variable from within {@link Block#declareInput(VarList)} or
+     * {@link Block#declareOutput(VarList)} with no particular runtime condition.
+     * 
+     * @param uid
+     *        the unique ID of the variable (within this list)
+     * @param variable
+     *        The variable to add
+     */
+    public void addRuntimeVariable(String uid, VarMutable variable)
+    {
+        add(uid, variable, true);
+    }
+
+    /**
+     * Adds the given variable to this list with the specified unique ID. Note that if a variable
+     * with same unique ID already exists, an {@link IllegalArgumentException} is thrown. <br/>
+     * <br/>
+     * WARNING: Do *not* mark a variable as dynamic if it is created and added from within
+     * {@link Block#declareInput(VarList)} or {@link Block#declareOutput(VarList)} with no
+     * particular runtime condition.
+     * 
+     * @param uid
+     *        the unique ID of the variable (within this list)
+     * @param variable
+     *        The variable to add
+     * @param isRuntimeVariable
+     *        <code>true</code> is the variable is dynamic (see {@link #isRuntimeVariable(Var)})
+     *        for more details.
+     * @throws IllegalArgumentException
+     *         if the variable already exists, or if a variable with same unique ID exists
+     */
+    private void add(String uid, Var<?> variable, boolean isRuntimeVariable)
+    {
+        if (varMap.containsKey(uid))
+            throw new IllegalArgumentException("A variable with same unique ID (" + uid + ") exists in the map");
+
+        varMap.put(uid, variable);
+
+        // By default, all block variables are visible
+        // However, variables from embedded blocks should not be visible
+        // (and should remain so until they are exposed by the user)
+        visibilityMap.put(variable, !uid.contains(":"));
+
+        runtimeVariableMap.put(variable, isRuntimeVariable);
+
+        for (VarListListener l : varListListeners)
+            l.variableAdded(this, variable);
+    }
+
+    /**
+     * Registers a new listener to receive events when a variable is added to this list
+     * 
+     * @param listener
+     *        Variable list listener to be added.
+     */
+    public void addVarListListener(VarListListener listener)
+    {
+        varListListeners.add(listener);
+    }
+
+    /**
+     * Registers a new listener to receive events when variables in this list change visibility
+     * 
+     * @param listener
+     *        Variable list listener to be added.
+     */
+    public void addVisibilityListener(VarVisibilityListener listener)
+    {
+        visibilityListeners.add(listener);
+    }
+
+    public void clear()
+    {
+        // remove elements one by one to notify listeners properly
+        ArrayList<Var<?>> vars = new ArrayList<Var<?>>(varMap.values());
+
+        for (Var<?> var : vars)
+            remove(var);
+
+        // FIXME this code does not remove links in the enclosing work flow...
+    }
+
+    public boolean contains(Var<?> variable)
+    {
+        return varMap.containsValue(variable);
+    }
+
+    /**
+     * @deprecated Legacy method (used to load old XML work flows).
+     * @param <T>
+     *        Type of the variable value.
+     * @param varID
+     *        Variable identifier.
+     * @return The stored variable.
+     * @throws NoSuchVariableException
+     *         If no variable is not found with the given identifier
+     */
+    @Deprecated
+    @SuppressWarnings("unchecked")
+    public <T> Var<T> get(int varID) throws NoSuchVariableException
+    {
+        int id = 0;
+        for (Var<?> var : this)
+        {
+            if (id == varID)
+                return (Var<T>) var;
+            id++;
+        }
+        throw new NoSuchVariableException("No variable with ID " + varID);
+    }
+
+    /**
+     * Generic access method to retrieve a variable from the map. This method uses generic types to
+     * prevent unchecked conversion warning in higher-level code.
+     * 
+     * @param <T>
+     *        Type of the variable value.
+     * @param uid
+     *        The unique ID of the variable to retrieve.
+     * @return The stored variable, or null if this name isn't in the map.
+     */
+    @SuppressWarnings("unchecked")
+    public <T> Var<T> get(String uid)
+    {
+        Var<?> var = varMap.get(uid);
+        return var == null ? null : (Var<T>) var;
+    }
+
+    /**
+     * Returns the unique ID of the specified variable. Although the underlying structure
+     * 
+     * @param var
+     *        Target variable
+     * @return Identifier of the given variable.
+     * @throws NoSuchVariableException
+     *         If given variable is not present in this variable list.
+     */
+    public String getID(Var<?> var) throws NoSuchVariableException
+    {
+        for (String uid : varMap.keySet())
+            if (varMap.get(uid) == var)
+                return uid;
+
+        throw new NoSuchVariableException("Variable " + var.getName() + " does not exist in this list");
+    }
+
+    /**
+     * @deprecated Variable index should not be used to refer to a variable
+     * @param variable
+     *        Target variable.
+     * @return The index of the list where the given variable is stored.
+     * @throws NoSuchVariableException
+     *         If the variable is not found in this variable list.
+     */
+    @Deprecated
+    public int indexOf(Var<?> variable) throws NoSuchVariableException
+    {
+        // throw new UnsupportedOperationException("Cannot retrieve the index of a variable");
+        // return varMap.indexOf(variable);
+        int index = 0;
+        for (Var<?> var : this)
+        {
+            if (variable == var)
+                return index;
+            index++;
+        }
+        throw new NoSuchVariableException(variable.getName());
+    }
+
+    /**
+     * Indicates whether the specified variable is dynamic.<br>
+     * A variable is considered dynamic if it has been added to the list at "design-time" (e.g. via
+     * the graphical user interface) rather than at "compile-time" (i.e. via
+     * {@link Block#declareInput(VarList)} or {@link Block#declareOutput(VarList)}). <br>
+     * A dynamic variable is marked with an additional attribute when stored in XML, such that it
+     * can be restored properly (via {@link VarMutable} objects) when the variable is reloaded from
+     * XML.
+     * 
+     * @param var
+     *        Variable to be checked.
+     * @return true if the variable is of type runtime.
+     */
+    public boolean isRuntimeVariable(Var<?> var)
+    {
+        return runtimeVariableMap.get(var);
+    }
+
+    public boolean isVisible(Var<?> var)
+    {
+        return visibilityMap.get(var);
+    }
+
+    @Override
+    public Iterator<Var<?>> iterator()
+    {
+        return varMap.values().iterator();
+    }
+
+    public void remove(Var<?> var)
+    {
+        if (var.getReference() != null)
+        {
+            var.setReference(null);
+            return;
+        }
+
+        varMap.remove(getID(var));
+        visibilityMap.remove(var);
+        runtimeVariableMap.remove(var);
+
+        for (VarListListener l : varListListeners)
+            l.variableRemoved(this, var);
+    }
+
+    /**
+     * Registers a new listener to receive events when a variable is removed from this list.
+     * 
+     * @param listener
+     *        Listener to remove.
+     */
+    public void removeVarListListener(VarListListener listener)
+    {
+        varListListeners.remove(listener);
+    }
+
+    /**
+     * Registers a new listener to receive events when variables in this list change visibility.
+     * 
+     * @param listener
+     *        Listener to remove.
+     */
+    public void removeVisibilityListener(VarVisibilityListener listener)
+    {
+        visibilityListeners.remove(listener);
+    }
+
+    /**
+     * Adjusts the visibility of the given variable outside the enclosing work flow.
+     * 
+     * @param var
+     *        Variable to set visibility.
+     * @param visible
+     *        true if the variable is to be visible. false otherwise.
+     */
+    public void setVisible(Var<?> var, boolean visible)
+    {
+        if (!visibilityMap.containsKey(var))
+            return;// throw new NoSuchVariableException(var);
+
+        if (visibilityMap.get(var) != visible)
+        {
+            visibilityMap.put(var, visible);
+
+            for (VarVisibilityListener l : visibilityListeners)
+                l.visibilityChanged(var, visible);
+        }
+    }
+
+    public int size()
+    {
+        return varMap.size();
+    }
+
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/VarListListener.java b/src/main/java/plugins/adufour/blocks/util/VarListListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9f1cddb337af5839a68a476af71b87863130d1f
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/VarListListener.java
@@ -0,0 +1,10 @@
+package plugins.adufour.blocks.util;
+
+import plugins.adufour.vars.lang.Var;
+
+public interface VarListListener
+{
+    void variableAdded(VarList list, Var<?> variable);
+    
+    void variableRemoved(VarList list, Var<?> variable);
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/VarVisibilityListener.java b/src/main/java/plugins/adufour/blocks/util/VarVisibilityListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..2df7f3a280d9311b16f29aeefa6b14174f4402f9
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/VarVisibilityListener.java
@@ -0,0 +1,8 @@
+package plugins.adufour.blocks.util;
+
+import plugins.adufour.vars.lang.Var;
+
+public interface VarVisibilityListener
+{
+	void visibilityChanged(Var<?> source, boolean isVisible);
+}
diff --git a/src/main/java/plugins/adufour/blocks/util/WorkFlowListener.java b/src/main/java/plugins/adufour/blocks/util/WorkFlowListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..29f431f63a6fb1908f3d27e6d2c9112ee7b7a307
--- /dev/null
+++ b/src/main/java/plugins/adufour/blocks/util/WorkFlowListener.java
@@ -0,0 +1,85 @@
+package plugins.adufour.blocks.util;
+
+import plugins.adufour.blocks.lang.BlockDescriptor;
+import plugins.adufour.blocks.lang.Link;
+import plugins.adufour.blocks.lang.WorkFlow;
+import plugins.adufour.blocks.lang.BlockDescriptor.BlockStatus;
+import plugins.adufour.vars.lang.Var;
+
+public interface WorkFlowListener
+{
+    static final String WORKFLOW_MODIFIED = "WORKFLOW_MODIFIED";
+
+    static final String WORKFLOW_REPLACED = "WORKFLOW_REPLACED";
+
+    /**
+     * Called when a block has been added to a work flow
+     * 
+     * @param source
+     *        The workflow on which the block has been added.
+     * @param addedBlock
+     *        The block that has been added.
+     */
+    void blockAdded(WorkFlow source, BlockDescriptor addedBlock);
+
+    void blockCollapsed(WorkFlow source, BlockDescriptor block, boolean collapsed);
+
+    void blockDimensionChanged(WorkFlow source, BlockDescriptor block, int newWidth, int newHeight);
+
+    void blockLocationChanged(WorkFlow source, BlockDescriptor block, int newX, int newY);
+
+    void blockStatusChanged(WorkFlow source, BlockDescriptor block, BlockStatus status);
+
+    void blockVariableAdded(WorkFlow source, BlockDescriptor block, Var<?> variable);
+
+    <T> void blockVariableChanged(WorkFlow source, BlockDescriptor block, Var<T> variable, T newValue);
+
+    /**
+     * Called when a block has been removed from a work flow
+     * 
+     * @param source
+     *        The workflow on which the block has been removed.
+     * @param removedBlock
+     *        The block that has been removed.
+     */
+    void blockRemoved(WorkFlow source, BlockDescriptor removedBlock);
+
+    /**
+     * Called when a link has been added to a work flow
+     * 
+     * @param source
+     *        The workflow to which the link has been added.
+     * @param addedLink
+     *        The link that has been added to the workflow.
+     */
+    void linkAdded(WorkFlow source, Link<?> addedLink);
+
+    /**
+     * Called when a link has been removed from a work flow
+     * 
+     * @param source
+     *        The workflow to which the link has been removed.
+     * @param removedLink
+     *        The link that has been added to the workflow.
+     */
+    void linkRemoved(WorkFlow source, Link<?> removedLink);
+
+    /**
+     * Called when the order of the blocks in the work flow has changed
+     * 
+     * @param source
+     *        The workflow that has been reordered.
+     */
+    void workFlowReordered(WorkFlow source);
+
+    /**
+     * Called when the work flow changes status (useful to display messages to the user via a status
+     * bar)
+     * 
+     * @param source
+     *        The workflow which its status has been changed.
+     * @param message
+     *        The message describing the change.
+     */
+    void statusChanged(WorkFlow source, String message);
+}
diff --git a/src/main/java/plugins/adufour/protocols/Protocols.java b/src/main/java/plugins/adufour/protocols/Protocols.java
index 202cd32d5e7f2c75fca4cbdff29455aa5c4161b5..056e59a749c5f84ff40f78942e79a5bc9811e5e8 100644
--- a/src/main/java/plugins/adufour/protocols/Protocols.java
+++ b/src/main/java/plugins/adufour/protocols/Protocols.java
@@ -20,6 +20,7 @@ import org.w3c.dom.Document;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 
+import icy.common.Version;
 import icy.file.FileUtil;
 import icy.main.Icy;
 import icy.plugin.PluginLoader;
@@ -93,14 +94,14 @@ public class Protocols extends PluginActionable
     }
 
     /**
-	 * Saves the current state of the Protocols interface and restarts it.
-	 * <p>
-	 * This method is useful when plug-ins have been modified (via the plug-in
-	 * loader) and requires Protocols to restart to take into account the new
-	 * changes
-	 * 
-	 * @throws TransformerFactoryConfigurationError
-	 */
+     * Saves the current state of the Protocols interface and restarts it.
+     * <p>
+     * This method is useful when plug-ins have been modified (via the plug-in
+     * loader) and requires Protocols to restart to take into account the new
+     * changes
+     * 
+     * @throws TransformerFactoryConfigurationError
+     */
     public void reload(final Document reloadingXML, final String reloadingPath)
     {
         // 0) avoid silly situations...
@@ -154,7 +155,7 @@ public class Protocols extends PluginActionable
         ThreadUtil.invokeLater(new Runnable()
         {
             @Override
-			public void run()
+            public void run()
             {
                 // 3) launch a new instance of the Protocols plug-in
                 try
@@ -388,4 +389,11 @@ public class Protocols extends PluginActionable
         if (mainFrame != null)
             mainFrame.getContentPane().dispatchEvent(key);
     }
+
+    public String getFriendlyVersion()
+    {
+        Version v = getDescriptor().getVersion();
+        return "Blocks engine v." + v.getMajor() + "." + v.getMinor();
+    }
+
 }
diff --git a/src/main/java/plugins/adufour/protocols/gui/MainFrame.java b/src/main/java/plugins/adufour/protocols/gui/MainFrame.java
index ffff065b26992f2769ed7eb3b5754c34340f2b4f..2049c117f6241dc23c2312522d988ebdca1f4dc5 100644
--- a/src/main/java/plugins/adufour/protocols/gui/MainFrame.java
+++ b/src/main/java/plugins/adufour/protocols/gui/MainFrame.java
@@ -53,7 +53,6 @@ import icy.system.IcyExceptionHandler;
 import icy.system.IcyHandledException;
 import icy.system.thread.ThreadUtil;
 import icy.util.XMLUtil;
-import plugins.adufour.blocks.Blocks;
 import plugins.adufour.blocks.lang.Block;
 import plugins.adufour.blocks.lang.BlockDescriptor;
 import plugins.adufour.blocks.lang.BlockDescriptor.BlockStatus;
@@ -107,7 +106,7 @@ public class MainFrame extends IcyFrame implements IcyFrameListener, ActionListe
 
     public MainFrame(Protocols pluginInstance)
     {
-        super("Protocols editor (" + new Blocks().getFriendlyVersion() + ")", true, true, true, true, true);
+        super("Protocols editor (" + pluginInstance.getFriendlyVersion() + ")", true, true, true, true, true);
 
         this.pluginInstance = pluginInstance;
 
diff --git a/src/main/resources/logo/icysoftware_icon-protocols-sdk.png b/src/main/resources/logo/icysoftware_icon-protocols-sdk.png
new file mode 100644
index 0000000000000000000000000000000000000000..21eb08f98fbf3e02dd68e1c96e5f2e435a1ecaaa
Binary files /dev/null and b/src/main/resources/logo/icysoftware_icon-protocols-sdk.png differ