Index: /trunk/src/org/apollo/gio/gesture/data/TrackAdjustmentGestureData.java
===================================================================
--- /trunk/src/org/apollo/gio/gesture/data/TrackAdjustmentGestureData.java	(revision 1097)
+++ /trunk/src/org/apollo/gio/gesture/data/TrackAdjustmentGestureData.java	(revision 1097)
@@ -0,0 +1,18 @@
+package org.apollo.gio.gesture.data;
+
+import org.expeditee.gio.gesture.data.ItemSpecificGestureData;
+
+public class TrackAdjustmentGestureData extends ItemSpecificGestureData
+{
+	private long _adjustment;
+	
+	public TrackAdjustmentGestureData(long adjustment)
+	{
+		_adjustment = adjustment;
+	}
+	
+	public long getAdjustment()
+	{
+		return _adjustment;
+	}
+}
Index: /trunk/src/org/expeditee/core/Anchoring.java
===================================================================
--- /trunk/src/org/expeditee/core/Anchoring.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Anchoring.java	(revision 1097)
@@ -0,0 +1,157 @@
+package org.expeditee.core;
+
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+
+/**
+ * Class for anchoring the position of items within the frame.
+ * Anchoring is when the position of the item is defined relative
+ * to the edge of the frame instead of being defined in absolute
+ * coordinates. Can't be anchored to the left and right edge at
+ * the same time, nor the top and bottom edge at the same time.
+ * 
+ * @author cts16
+ */
+public class Anchoring {
+
+	/** The relative distance from the left or right edge (null means unanchored). */
+	private Integer _xAnchor = null;
+	/** The relative distance from the top or bottom edge (null means unanchored). */
+	private Integer _yAnchor = null;
+	/** Whether the x-anchor is relative to the left or right edge. */
+	private boolean _left = true;
+	/** Whether the y-anchor is relative to the top or bottom edge. */
+	private boolean _top = true;
+	
+	public Anchoring()
+	{
+	}
+	
+	public void setXAnchor(Integer offset, boolean left)
+	{
+		_xAnchor = offset;
+		_left = left;
+	}
+	
+	public void setYAnchor(Integer offset, boolean top)
+	{
+		_yAnchor = offset;
+		_top = top;
+	}
+	
+	public void setXAnchor(Anchoring other)
+	{
+		if (other == null) return;
+		
+		setXAnchor(other._xAnchor, other._left);
+	}
+
+	public void setYAnchor(Anchoring other)
+	{
+		if (other == null) return;
+		
+		setYAnchor(other._yAnchor, other._top);
+	}
+	
+	public void setLeftAnchor(Integer offset)
+	{
+		setXAnchor(offset, true);
+	}
+	
+	public void setRightAnchor(Integer offset)
+	{
+		setXAnchor(offset, false);
+	}
+	
+	public void setTopAnchor(Integer offset)
+	{
+		setYAnchor(offset, true);
+	}
+	
+	public void setBottomAnchor(Integer offset)
+	{
+		setYAnchor(offset, false);
+	}
+	
+	public void removeXAnchor()
+	{
+		setXAnchor(null, _left);
+	}
+	
+	public void removeYAnchor()
+	{
+		setYAnchor(null, _top);
+	}
+	
+	public void removeAllAnchors()
+	{
+		removeXAnchor();
+		removeYAnchor();
+	}
+	
+	public Integer getXPositionInBox(AxisAlignedBoxBounds box)
+	{
+		if (box == null || _xAnchor == null) return null;
+		
+		if (_left) {
+			return box.getMinX() + _xAnchor;
+		} else {
+			return box.getMaxX() - _xAnchor;
+		}
+	}
+	
+	public Integer getYPositionInBox(AxisAlignedBoxBounds box)
+	{
+		if (box == null || _yAnchor == null) return null;
+		
+		if (_top) {
+			return box.getMinY() + _yAnchor;
+		} else {
+			return box.getMaxY() - _yAnchor;
+		}
+	}
+	
+	public Point getPositionInBox(AxisAlignedBoxBounds box)
+	{
+		Integer x = getXPositionInBox(box);
+		Integer y = getYPositionInBox(box);
+		
+		if (x == null || y == null) return null;
+		
+		return new Point(x, y);
+	}
+	
+	public boolean isXAnchored()
+	{
+		return _xAnchor != null;
+	}
+	
+	public boolean isYAnchored()
+	{
+		return _yAnchor != null;
+	}
+	
+	public boolean isAnchored()
+	{
+		return isXAnchored() || isYAnchored();
+	}
+	
+	public Integer getLeftAnchor()
+	{
+		return _left ? _xAnchor : null;
+	}
+	
+	public Integer getRightAnchor()
+	{
+		return !_left ? _xAnchor : null;
+	}
+	
+	public Integer getTopAnchor()
+	{
+		return _top ? _yAnchor : null;
+	}
+	
+	public Integer getBottomAnchor()
+	{
+		return !_top ? _yAnchor : null;
+	}
+}
Index: /trunk/src/org/expeditee/core/BlockingRunnable.java
===================================================================
--- /trunk/src/org/expeditee/core/BlockingRunnable.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/BlockingRunnable.java	(revision 1097)
@@ -0,0 +1,52 @@
+package org.expeditee.core;
+
+/**
+ * A Runnable which can be asked to block the calling thread until
+ * execution of the given code has completed. Runnable's run() method
+ * is used as part of the implementation so user code should be specified
+ * by overriding the execute() method instead.
+ * 
+ * @author cts16
+ */
+public abstract class BlockingRunnable implements Runnable {
+
+	/** Whether the code has finished executing or not. */
+	private boolean _done;
+	/** Synchronisation object to prevent race conditions. */
+	private Object _lock;
+	
+	public BlockingRunnable()
+	{
+		_done = false;
+		_lock = new Object();
+	}
+	
+	/** Should be overridden to specify user behaviour. */
+	public abstract void execute();
+	
+	@Override
+	public final void run()
+	{
+		execute();
+		
+		// Mark as finished and wake any blocked threads
+		synchronized (_lock) {
+			_done = true;
+			_lock.notifyAll();
+		}
+	}
+	
+	/** Can be called to block the calling thread until the execute() method returns. */
+	public final void blockUntilDone()
+	{
+		synchronized (_lock) {
+			while (!_done) {
+				try {
+					_lock.wait();
+				} catch (InterruptedException e) {
+					// Do nothing, just try waiting again
+				}
+			}
+		}
+	}
+}
Index: /trunk/src/org/expeditee/core/Clip.java
===================================================================
--- /trunk/src/org/expeditee/core/Clip.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Clip.java	(revision 1097)
@@ -0,0 +1,141 @@
+package org.expeditee.core;
+
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+
+/**
+ * An area to clip drawing operations to.
+ * 
+ * @author cts16
+ */
+public class Clip {
+	
+	/** The bounds of this clip. Null means there is no clipping. */
+	private AxisAlignedBoxBounds _bounds = null;
+	
+	/** Flag to identify when there is no unclipped area. */
+	private boolean _isFullyClipped = false;
+	
+	/** Default constructor. */
+	public Clip()
+	{
+	}
+	
+	/** Construct a clip encompassing the given bounds. */
+	public Clip(AxisAlignedBoxBounds bounds)
+	{
+		this(bounds, false);
+	}
+	
+	/** Standard constructor. */
+	protected Clip(AxisAlignedBoxBounds bounds, boolean isFullyClipped)
+	{
+		if (bounds != null) _bounds = bounds.clone();
+		_isFullyClipped = isFullyClipped;
+	}
+	
+	/** Copy constructor. */
+	public Clip(Clip other)
+	{
+		if (other._bounds != null) _bounds = other._bounds.clone();
+		_isFullyClipped = other._isFullyClipped;
+	}
+
+	/** Creates a clip which clips the entire area. */
+	public static Clip getFullClip()
+	{
+		return new Clip(null, true);
+	}
+	
+	/** Checks if this clip clips all possible space. */
+	public boolean isFullyClipped()
+	{
+		return _isFullyClipped;
+	}
+	
+	/** Checks if this clip is not clipping anything. */
+	public boolean isNotClipped()
+	{
+		return !isFullyClipped() && _bounds == null;
+	}
+	
+	/**
+	 * Gets the bounds of the clip. Null means no clipping.
+	 * Should check isFullyClipped() to see if the entire area is clipped.
+	 */
+	public AxisAlignedBoxBounds getBounds()
+	{
+		if (_bounds == null) return null;
+		
+		return _bounds.clone();
+	}
+	
+	/** Resets the clip so that no area is clipped. */
+	public Clip clear()
+	{
+		_bounds = null;
+		_isFullyClipped = false;
+		
+		return this;
+	}
+	
+	/** Expands the clip area to incorporate the given bounds. */
+	public Clip includeBounds(AxisAlignedBoxBounds bounds)
+	{
+		if (bounds == null) return this;
+		
+		// If we are fully clipped then the given bounds will be the only unclipped area
+		if (isFullyClipped()) {
+			_isFullyClipped = false;
+			_bounds = bounds.clone();
+			
+		// If _bounds is null then we already contain the given bounds
+		} else if (_bounds != null) {
+			_bounds = _bounds.combineWith(bounds);
+		}
+		
+		return this;
+	}
+	
+	/** Updates the clip to include only those areas that are inside the given bounds. */
+	public Clip intersectWith(AxisAlignedBoxBounds bounds)
+	{
+		if (bounds == null) return this;
+		
+		// If fully clipped then no area is inside the given bounds
+		if (isFullyClipped()) return this;
+		
+		// If there is no clipped area then the given bounds defines the intersection
+		if (_bounds == null) {
+			_bounds = bounds.clone();
+		} else {
+			AxisAlignedBoxBounds newClip = _bounds.intersectionWith(bounds);
+			
+			// If the current bounds and given bounds don't intersect, the area is fully clipped
+			if (newClip == null) {
+				_isFullyClipped = true;
+			} else {
+				_bounds = newClip;
+			}
+		}
+		
+		return this;
+	}
+	
+	/** Updates the clip to include only those areas that are inside the given clip. */
+	public Clip intersectWith(Clip clip)
+	{
+		if (clip == null) return this;
+		
+		if (!clip.isFullyClipped()) return intersectWith(clip.getBounds());
+		
+		_isFullyClipped = true;
+		
+		return this;
+	}
+	
+	@Override
+	public Clip clone()
+	{
+		return new Clip(this);
+	}
+}
Index: /trunk/src/org/expeditee/core/Colour.java
===================================================================
--- /trunk/src/org/expeditee/core/Colour.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Colour.java	(revision 1097)
@@ -0,0 +1,282 @@
+package org.expeditee.core;
+
+/**
+ * Class representing all colour information in Expeditee.
+ * Internally uses 15-bit channels, but has helper methods for converting
+ * to and from standard 8-bit channels.
+ * 
+ * @author cts16
+ *
+ */
+public class Colour {
+
+	private short r, g, b, a;
+	
+	public static final short COMPONENT_MAX_VALUE = Short.MAX_VALUE;
+	public static final short COMPONENT_MIN_VALUE = (short) 0;
+	
+	// Constant colour definitions
+	public static final Colour WHITE = new Colour(COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour RED = new Colour(COMPONENT_MAX_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour GREEN = new Colour(COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour BLUE = new Colour(COMPONENT_MIN_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour CYAN = new Colour(COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour MAGENTA = new Colour(COMPONENT_MAX_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour YELLOW = new Colour(COMPONENT_MAX_VALUE, COMPONENT_MAX_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour BLACK = new Colour(COMPONENT_MIN_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MIN_VALUE, COMPONENT_MAX_VALUE);
+	public static final Colour GREY = FromRGBA255(128, 128, 128, 255);
+	public static final Colour DARK_GREY = FromRGBA255(64, 64, 64, 255);
+	public static final Colour LIGHT_GREY = FromRGBA255(192, 192, 192, 255);
+	public static final Colour ORANGE = FromRGBA255(255, 200, 0, 255);
+	public static final Colour PINK = FromRGBA255(255, 175, 175, 255);
+	public static final Colour TRANSPARENT = new Colour();
+	
+	// Spelling translations of 'grey' to 'gray' to conform to AWT Color names
+	// (because these can be reflected and so might break something if not available)
+	public static final Colour GRAY = GREY;
+	public static final Colour DARK_GRAY = DARK_GREY;
+	public static final Colour LIGHT_GRAY = LIGHT_GREY;
+	
+	/**
+	 * Default construct gives transparent black.
+	 */
+	public Colour()
+	{
+		this(0, 0, 0, 0);
+	}
+	
+	/**
+	 * Standard constructor taking each component value as a parameter.
+	 * 
+	 * @param r Red channel
+	 * @param g Green channel
+	 * @param b Blue channel
+	 * @param a Alpha channel
+	 */
+	public Colour(short r, short g, short b, short a)
+	{
+		assert(COMPONENT_MIN_VALUE <= r && r <= COMPONENT_MAX_VALUE);
+		assert(COMPONENT_MIN_VALUE <= g && g <= COMPONENT_MAX_VALUE);
+		assert(COMPONENT_MIN_VALUE <= b && b <= COMPONENT_MAX_VALUE);
+		assert(COMPONENT_MIN_VALUE <= a && a <= COMPONENT_MAX_VALUE);
+		
+		this.r = r;
+		this.g = g;
+		this.b = b;
+		this.a = a;
+	}
+	
+	/** Clone constructor. */
+	public Colour(Colour other)
+	{
+		this(other.r, other.g, other.b, other.a);
+	}
+	
+	/** Convenience constructor taking r,g,b,a as ints. */
+	public Colour(int r, int g, int b, int a)
+	{
+		this((short) r,(short) g,(short) b,(short) a);
+	}
+	
+	/** Convenience constructor taking r,g,b,a as floats in the range 0.0f to 1.0f. */
+	public Colour(float r, float g, float b, float a)
+	{
+		this(FromComponentFloat(r), FromComponentFloat(g), FromComponentFloat(b), FromComponentFloat(a));
+	}
+	
+	/** Constructor defaults alpha to completely opaque. */
+	public Colour(short r, short g, short b)
+	{
+		this(r, g, b, COMPONENT_MAX_VALUE);
+	}
+	
+	/** Constructor defaults alpha to completely opaque. */
+	public Colour(int r, int g, int b)
+	{
+		this((short) r,(short) g,(short) b);
+	}
+	
+	/** Constructor defaults alpha to completely opaque. */
+	public Colour(float r, float g, float b)
+	{
+		this(r, g, b, 1.0f);
+	}
+	
+	/** Creates a colour from r,g,b,a values in the range 0 - 255. */
+	public static Colour FromRGBA255(int r, int g, int b, int a)
+	{
+		return new Colour(FromComponent255(r), FromComponent255(g), FromComponent255(b), FromComponent255(a));
+	}
+
+	/** Creates a colour from r,g,b values in the range 0 - 255. */
+	public static Colour FromRGB255(int r, int g, int b)
+	{
+		return new Colour(FromComponent255(r), FromComponent255(g), FromComponent255(b));
+	}
+
+	/**
+	 * Converts a single component value from the range 0 - 255 into
+	 * the internal colour range.
+	 */
+	public static short FromComponent255(int component)
+	{
+		assert(0 <= component && component <= 255);
+		
+		return (short) (component << 7);
+	}
+
+	/**
+	 * Converts a single component value from the internal colour range into
+	 * the range 0 - 255.
+	 */
+	public static int ToComponent255(short component)
+	{
+		assert(COMPONENT_MIN_VALUE <= component && component <= COMPONENT_MAX_VALUE);
+		
+		return ((int) component) >> 7;
+	}
+
+	/**
+	 * Converts a single component value from the range 0.0f - 1.0f into
+	 * the internal colour range.
+	 */
+	public static short FromComponentFloat(float component)
+	{
+		assert(0.0f <= component && component <= 1.0f);
+		
+		return (short) (component * COMPONENT_MAX_VALUE);
+	}
+
+	/**
+	 * Converts a single component value from the internal colour range into
+	 * the range 0.0f - 1.0f.
+	 */
+	public static float ToComponentFloat(short component)
+	{
+		assert(COMPONENT_MIN_VALUE <= component && component <= COMPONENT_MAX_VALUE);
+		
+		return ((float) component) / COMPONENT_MAX_VALUE;
+	}
+	
+	@Override
+	public boolean equals(Object other)
+	{
+		if (other instanceof Colour)
+		{
+			Colour c = (Colour) other;
+			return (r == c.r && g == c.g && b == c.b && a == c.a);
+		}
+		
+		return false;
+	}
+	
+	/** Returns a new colour which is this colour made brighter. */
+	public Colour brighter() {
+        int r = getRed();
+        int g = getGreen();
+        int b = getBlue();
+        int alpha = getAlpha();
+        
+        int i = FromComponent255((int)(1.0/0.7));
+        if ( r == 0 && g == 0 && b == 0) {
+            return new Colour(i, i, i, alpha);
+        }
+        if ( r > 0 && r < i ) r = i;
+        if ( g > 0 && g < i ) g = i;
+        if ( b > 0 && b < i ) b = i;
+
+        return new Colour(r, g, b, alpha).scale(1.0/0.7);
+    }
+	
+	/** Returns a new colour which is this colour made darker. */
+	public Colour darker()
+	{
+		return scale(0.7);
+	}
+	
+	/**
+	 * Returns a new colour which is this colour with the r,g,b
+	 * components by the given scale factor.
+	 */
+	public Colour scale(double factor)
+	{
+		if (factor < 0.0) return null;
+		
+		double newR = r * factor;
+		double newG = g * factor;
+		double newB = b * factor;
+		
+		if (newR > COMPONENT_MAX_VALUE) newR = COMPONENT_MAX_VALUE;
+		if (newG > COMPONENT_MAX_VALUE) newG = COMPONENT_MAX_VALUE;
+		if (newB > COMPONENT_MAX_VALUE) newB = COMPONENT_MAX_VALUE;
+		
+		return new Colour((short) newR, (short) newG, (short) newB, a);
+	}
+	
+	public short getRed() {	return r; }
+	public short getGreen() {	return g; }
+	public short getBlue() {	return b; }
+	public short getAlpha() {	return a; }
+	
+	public void setRed(short r) {	this.r = r; }
+	public void setGreen(short g) {	this.g = g; }
+	public void setBlue(short b) {	this.b = b; }
+	public void setAlpha(short a) {	this.a = a; }
+	
+	public int getRed255() { return ToComponent255(r); }
+	public int getGreen255() { return ToComponent255(g); }
+	public int getBlue255() { return ToComponent255(b); }
+	public int getAlpha255() { return ToComponent255(a); }
+	
+	public float getRedFloat() { return ToComponentFloat(r); }
+	public float getGreenFloat() { return ToComponentFloat(g); }
+	public float getBlueFloat() { return ToComponentFloat(b); }
+	public float getAlphaFloat() { return ToComponentFloat(a); }
+	
+	/** Creates a new colour which is the inverse of this colour. */
+	public Colour inverse()
+	{
+		return new Colour(COMPONENT_MAX_VALUE - r, COMPONENT_MAX_VALUE - g, COMPONENT_MAX_VALUE - b, a);
+	}
+	
+	/**
+	 * Decodes a string into a colour. The format of the string should be
+	 * a packed r,g,b integer value e.g. "0xrrggbb". 
+	 */
+	public static Colour decode(String nm) throws NumberFormatException {
+        Integer intval = Integer.decode(nm);
+        int i = intval.intValue();
+        return fromRGB24BitPacked(i);
+    }
+	
+	/**
+	 * Returns an integer representing this colour as a 32-bit packed
+	 * integer i.e. 0xaarrggbb
+	 */
+	public int getARGB32BitPacked()
+	{
+		return getBlue255() | (getGreen255() << 8) | (getRed255() << 16) | (getAlpha255() << 24);
+	}
+	
+	/** Creates a colour from a 32-bit packed integer in to form 0xaarrggbb. */
+	public static Colour fromARGB32BitPacked(int rgba)
+	{
+		int a = (rgba >> 24) & 0xFF;
+		int r = (rgba >> 16) & 0xFF;
+		int g = (rgba >> 8) & 0xFF;
+		int b = rgba & 0xFF;
+		
+		return FromRGBA255(r, g, b, a);
+	}
+
+	/** Creates a colour from a 24-bit packed integer in to form 0xrrggbb. */
+	public static Colour fromRGB24BitPacked(int rgb)
+	{
+		return fromARGB32BitPacked(rgb | 0xFF000000);
+	}
+	
+	public Colour clone()
+	{
+		return new Colour(this);
+	}
+}
Index: /trunk/src/org/expeditee/core/Cursor.java
===================================================================
--- /trunk/src/org/expeditee/core/Cursor.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Cursor.java	(revision 1097)
@@ -0,0 +1,77 @@
+package org.expeditee.core;
+
+import org.expeditee.gui.DisplayController;
+
+/**
+ * TODO: Comment. cts16
+ * 
+ * @author cts16
+ */
+public class Cursor {
+
+	public enum CursorType {
+		DEFAULT,
+		TEXT,
+		CROSSHAIR,
+		CUSTOM
+	}
+	
+	/** Caches the invisible cursor. */
+	private static Cursor _invisibleCursor = null;
+	
+	private Image _image;
+	private Point _hotspot;
+	private String _name;
+	private CursorType _type;
+	
+	public Cursor(Image image, Point hotspot, String name)
+	{
+		assert(image != null && hotspot != null && name != null);
+		
+		_image = image;
+		_hotspot = hotspot.clone();
+		_name = name;
+		_type = CursorType.CUSTOM;
+	}
+	
+	public Cursor(CursorType type)
+	{
+		_image = null;
+		_hotspot = null;
+		_name = null;
+		_type = type;
+	}
+	
+	public Image getImage()
+	{
+		return _image;
+	}
+	
+	public Point getHotspot()
+	{
+		return _hotspot.clone();
+	}
+	
+	public String getName()
+	{
+		return _name;
+	}
+	
+	public CursorType getType()
+	{
+		return _type;
+	}
+	
+	/** Creates an invisible cursor. */
+	public static Cursor createInvisibleCursor()
+	{
+		if (_invisibleCursor == null) {
+			int[] pixels = new int[DisplayController.SMALL_CURSOR_SIZE * DisplayController.SMALL_CURSOR_SIZE];
+			Image image = Image.createImage(DisplayController.SMALL_CURSOR_SIZE, DisplayController.SMALL_CURSOR_SIZE, pixels);
+			_invisibleCursor = new Cursor(image, new Point(0, 0), "invisiblecursor");
+		}
+		
+		return _invisibleCursor;
+	}
+	
+}
Index: /trunk/src/org/expeditee/core/DamageAreas.java
===================================================================
--- /trunk/src/org/expeditee/core/DamageAreas.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/DamageAreas.java	(revision 1097)
@@ -0,0 +1,52 @@
+package org.expeditee.core;
+
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.core.bounds.Bounds;
+import org.expeditee.core.bounds.CombinationBounds;
+
+/**
+ * Handles the accumulation of damage areas and clip generation.
+ * 
+ * @author cts16
+ */
+public class DamageAreas {
+
+	private CombinationBounds _damagedAreas = null;
+	
+	public DamageAreas()
+	{
+		// Do nothing (member allocated on use)
+	}
+	
+	/** Adds an area to the damaged areas. */
+	public synchronized void addArea(Bounds area)
+	{
+		if (_damagedAreas == null) {
+			_damagedAreas = new CombinationBounds(area);
+		} else {
+			_damagedAreas.add(area);
+		}
+	}
+	
+	/** Resets the damaged areas to no area damaged. */
+	public synchronized void clear()
+	{
+		_damagedAreas = null;
+	}
+	
+	/**
+	 * Returns the rectangular clip enclosing the damaged areas.
+	 * Clears the clip at the same time.
+	 */
+	public synchronized Clip getClip()
+	{
+		AxisAlignedBoxBounds bounds = AxisAlignedBoxBounds.getEnclosing(_damagedAreas);
+		clear();
+		if (bounds != null) {
+			return new Clip(bounds);
+		} else {
+			return Clip.getFullClip();
+		}
+	}
+	
+}
Index: /trunk/src/org/expeditee/core/Dimension.java
===================================================================
--- /trunk/src/org/expeditee/core/Dimension.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Dimension.java	(revision 1097)
@@ -0,0 +1,80 @@
+package org.expeditee.core;
+
+/**
+ * A Dimension represents the 2-dimensional size of something.
+ * 
+ * @author cts16
+ */
+public class Dimension {
+
+	/** The size in the x-dimension. */
+	public int width;
+	/** The size in the y-dimension. */
+	public int height;
+	
+	/** Defaults to a zero-area (degenerate) size if no arguments are given. */
+	public Dimension()
+	{
+		this(0, 0);
+	}
+	
+	/** Standard constructor. */
+	public Dimension(int width, int height)
+	{
+		this.width = width;
+		this.height = height;
+	}
+	
+	/** Copy constructor. */
+	public Dimension(Dimension other)
+	{
+		if (other != null) {
+			this.width = other.width;
+			this.height = other.height;
+		} else {
+			this.width = 0;
+			this.height = 0;
+		}
+	}
+	
+	/** Gets the width of this dimension. */
+	public int getWidth()
+	{
+		return width;
+	}
+	
+	/** Gets the height of this dimension. */
+	public int getHeight()
+	{
+		return height;
+	}
+	
+	/** Gets the rectangular area of this dimension. */
+	public int getArea()
+	{
+		return width * height;
+	}
+	
+	/** Sets any negative widths/heights to the absolute equivalents. */
+	public void absolve()
+	{
+		if (width < 0) width = -width;
+		if (height <0) height = -height;
+	}
+	
+	@Override
+	public boolean equals(Object other)
+	{
+		if (other instanceof Dimension) {
+			Dimension d = (Dimension) other;
+			return this.width == d.width && this.height == d.height;
+		}
+		
+		return false;
+	}
+	
+	public Dimension clone()
+	{
+		return new Dimension(this);
+	}
+}
Index: /trunk/src/org/expeditee/core/EcosystemSpecific.java
===================================================================
--- /trunk/src/org/expeditee/core/EcosystemSpecific.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/EcosystemSpecific.java	(revision 1097)
@@ -0,0 +1,23 @@
+package org.expeditee.core;
+
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.EcosystemManager.Ecosystem;
+
+/**
+ * This interface should be implemented by any class that can only be used
+ * in a specific set of ecosystems.
+ * 
+ * @author cts16
+ */
+public interface EcosystemSpecific
+{
+	/** Whether the class is supported on the given ecosystem. */
+	public boolean isSupportedOnEcosystem(Ecosystem ecosystem);
+	
+	/** Whether the class is supported on the current ecosystem. */
+	public default boolean isSupportedOnCurrentEcosystem()
+	{
+		return isSupportedOnEcosystem(EcosystemManager.getType());
+	}
+	
+}
Index: /trunk/src/org/expeditee/core/EnforcedClipStack.java
===================================================================
--- /trunk/src/org/expeditee/core/EnforcedClipStack.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/EnforcedClipStack.java	(revision 1097)
@@ -0,0 +1,81 @@
+package org.expeditee.core;
+
+import java.util.Stack;
+
+public class EnforcedClipStack {
+
+	/** The stack of sub-area clips. */
+	private Stack<Clip> _clipStack;
+	
+	/** Default constructor. */
+	public EnforcedClipStack()
+	{
+		_clipStack = new Stack<Clip>();
+	}
+	
+	/** Pushes a new clip onto the stack, ensuring that it is a sub-area of the top of the stack. */
+	public EnforcedClipKey push(Clip clip)
+	{
+		// No null clips allowed
+		if (clip == null) return null;
+		
+		// Leave the given clip unchanged (in case the provider wants to keep using it)
+		clip = clip.clone();
+		
+		// Each clip must be a sub-area of the one below it in the stack
+		if (!_clipStack.isEmpty()) {
+			clip.intersectWith(_clipStack.peek());
+		}
+		
+		// Put the new clip on the stack
+		_clipStack.push(clip);
+		
+		// Return the clipped area
+		return EnforcedClipKey.get(clip);
+	}
+	
+	/** Pops the top of the stack if there given key corresponds to it. */
+	public Clip pop(EnforcedClipKey key)
+	{
+		if (!_clipStack.isEmpty() && key != null && key.matches(_clipStack.peek())) return _clipStack.pop();
+		
+		return null;
+	}
+	
+	/** Returns a copy of the current top of the stack. */
+	public Clip peek()
+	{
+		if (!_clipStack.isEmpty()) return _clipStack.peek().clone();
+		
+		return null;
+	}
+	
+	/** Key class to ensure only the owner of a given pushed clip can pop it again. */
+	public static class EnforcedClipKey {
+		
+		/** The clip this key corresponds to. */
+		private Clip _keyedClip;
+		
+		/** Gets a key for the given clip. */
+		public static EnforcedClipKey get(Clip clip)
+		{
+			if (clip == null) return null;
+			
+			return new EnforcedClipKey(clip);
+		}
+		
+		/** Private constructor so clip cannot be null. */
+		private EnforcedClipKey(Clip clip)
+		{
+			_keyedClip = clip;
+		}
+		
+		/** Whether this key corresponds to the given clip. */
+		public boolean matches(Clip clip)
+		{
+			return clip == _keyedClip;
+		}
+		
+	}
+	
+}
Index: /trunk/src/org/expeditee/core/FSM.java
===================================================================
--- /trunk/src/org/expeditee/core/FSM.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/FSM.java	(revision 1097)
@@ -0,0 +1,128 @@
+package org.expeditee.core;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FSM {
+
+	private int _currentState;
+	private List<Object> _transitions;
+	private List<Integer> _nextTrueStates;
+	private List<Integer> _nextFalseStates;
+	private List<Boolean> _finalStates;
+	private boolean _inconsistent;
+	
+	public FSM()
+	{
+		_transitions = new ArrayList<Object>();
+		_nextTrueStates = new ArrayList<Integer>();
+		_nextFalseStates = new ArrayList<Integer>();
+		_finalStates = new ArrayList<Boolean>();
+		
+		configureState(0, null, 0, 0, false);
+		reset();
+	}
+	
+	/**
+	 * Resets the DFSM to its initial state.
+	 */
+	public void reset()
+	{
+		_currentState = 0;
+		_inconsistent = false;
+	}
+	
+	/**
+	 * Gets the current state of the DFSM.
+	 */
+	public int currentState()
+	{
+		return _currentState;
+	}
+	
+	/**
+	 * Whether the DFSM is in a consistent state. May be false if a state has been incorrectly
+	 * configured causing a transition into unconfigured territory.
+	 */
+	public boolean isConsistent()
+	{
+		return !_inconsistent;
+	}
+	
+	/**
+	 * Edits the configuration of a given state.
+	 * 
+	 * @param state The state to configure.
+	 * @param transition The object to use for comparison to determine the next state.
+	 * @param nextTrueState The next state to go to if the input is equal to the transition object.
+	 * @param nextFalseState The next state to go to if the input is not equal to the transition object.
+	 * @param isFinalState Whether this state is a final state.
+	 */
+	public void configureState(int state, Object transition, int nextTrueState, int nextFalseState, boolean isFinalState)
+	{
+		if (state < 0 || nextTrueState < 0 || nextFalseState < 0) return;
+		
+		while (state > _transitions.size()) {
+			_transitions.add(null);
+			_nextTrueStates.add(null);
+			_nextFalseStates.add(null);
+			_finalStates.add(null);
+		}
+		
+		_transitions.set(state, transition);
+		_nextTrueStates.set(state, nextTrueState);
+		_nextFalseStates.set(state, nextFalseState);
+		_finalStates.set(state, isFinalState);
+	}
+	
+	/**
+	 * Moves the DFSM to a new state based on comparing the input object to the transition
+	 * object for the current state.
+	 * 
+	 * @param input The input object to compare to.
+	 */
+	public void input(Object input)
+	{
+		// Can't do anything if we are inconsistent
+		if (_inconsistent) return;
+		
+		// Get the transition object for the current state
+		Object transition = _transitions.get(_currentState);
+		
+		// Work out whether the input matches the transition object
+		boolean result;
+		if (transition == null) {
+			result = input == null;
+		} else {
+			result = transition.equals(input);
+		}
+		
+		// Work out which state to go to next
+		Integer nextState;
+		if (result) {
+			nextState = _nextTrueStates.get(_currentState);
+		} else {
+			nextState = _nextFalseStates.get(_currentState);
+		}
+		
+		// If there is no next state, we are inconsistent
+		if (nextState == null) {
+			_inconsistent = true;
+			return;
+		}
+		
+		// Go to the next state
+		_currentState = nextState;
+	}
+	
+	/**
+	 * Whether or not this DFSM is currently in a final state.
+	 */
+	public boolean isFinalState()
+	{
+		Boolean finalState = _finalStates.get(_currentState);
+		return !_inconsistent && finalState != null && finalState.booleanValue();
+	}
+	
+	
+}
Index: /trunk/src/org/expeditee/core/Fill.java
===================================================================
--- /trunk/src/org/expeditee/core/Fill.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Fill.java	(revision 1097)
@@ -0,0 +1,39 @@
+package org.expeditee.core;
+
+/**
+ * Represents a way of filling in an area with colour. This base Fill
+ * class represents just filling the entire area with a single solid
+ * colour, but inheriting classes may add other modes of filling.
+ * 
+ * @author cts16
+ */
+public class Fill {
+
+	/** The colour with which to fill an area. */
+	private Colour _colour;
+	
+	/** Standard constructor. */
+	public Fill(Colour colour)
+	{
+		setColour(colour);
+	}
+	
+	/** Default constructor defaults to whatever the default Colour constructor provides. */
+	public Fill()
+	{
+		setColour(new Colour());
+	}
+	
+	/** Sets the fill colour. */
+	public void setColour(Colour colour)
+	{
+		_colour = colour;
+	}
+	
+	/** Gets the fill colour. */
+	public Colour getColour()
+	{
+		return _colour;
+	}
+	
+}
Index: /trunk/src/org/expeditee/core/Font.java
===================================================================
--- /trunk/src/org/expeditee/core/Font.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Font.java	(revision 1097)
@@ -0,0 +1,225 @@
+package org.expeditee.core;
+
+/**
+ * Handle-type for fonts managed by the font manager. Should be able to
+ * instantiate your own font objects, and have the font manager use an appropriate
+ * internal font when necessary (i.e. Font objects don't need to be constructed by
+ * the font manager, they are interpreted on use).
+ * 
+ * @author cts16
+ */
+public class Font {
+
+	/** Enumeration of the possible styles of font (bold/italic). */
+	public enum Style {
+		PLAIN,
+		BOLD,
+		ITALIC,
+		BOLD_ITALIC
+	}
+
+	/** The default family for fonts which don't explicitly specify one. */
+	public static final String DEFAULT_FAMILY_NAME = "Serif";
+	/** The default style for fonts which don't explicitly specify one. */
+	public static final Font.Style DEFAULT_STYLE = Font.Style.PLAIN;
+	/** The default size for fonts which don't explicitly specify one. */
+	public static final int DEFAULT_SIZE = 12;
+	/** The default spacing for fonts which don't explicitly specify one. */
+	public static final float DEFAULT_SPACING = 0.0f;
+	
+	/** The family name of this font. */
+	private String _familyName;
+	/** The style of this font (bold/italic). */
+	private Style _style;
+	/** The size of this font. */
+	private int _size;
+	/** The spacing of this font. */
+	private float _spacing;
+	
+	/** Standard constructor. */
+	public Font(String familyName, Style style, int size, float spacing)
+	{
+		if (familyName == null) familyName = DEFAULT_FAMILY_NAME;
+		if (style == null) style = DEFAULT_STYLE;
+		
+		_familyName = familyName;
+		_style = style;
+		_size = size;
+		_spacing = spacing;
+	}
+	
+	/** Convenience constructor. */
+	public Font(String familyName, Style style, int size)
+	{
+		this(familyName, style, size, DEFAULT_SPACING);
+	}
+
+	/** Convenience constructor. */
+	public Font(String familyName, Style style)
+	{
+		this(familyName, style, DEFAULT_SIZE);
+	}
+
+	/** Convenience constructor. */
+	public Font(String familyName)
+	{
+		this(familyName, DEFAULT_STYLE);
+	}
+
+	/** Convenience constructor. */
+	public Font()
+	{
+		this(DEFAULT_FAMILY_NAME);
+	}
+	
+	/** Copy constructor. */
+	public Font(Font other)
+	{
+		_familyName = other._familyName;
+		_style = other._style;
+		_size = other._size;
+		_spacing = other._spacing;
+	}
+	
+	/** Gets the family name of this font. */
+	public String getFamilyName()
+	{
+		return _familyName;
+	}
+	
+	/** Sets the style of this font. */
+	public Font setStyle(Style style)
+	{
+		_style = style;
+		
+		return this;
+	}
+
+	/** Gets the style of this font. */
+	public Style getStyle()
+	{
+		return _style;
+	}
+	
+	/** Returns true if this font has bold styling. */
+	public boolean isBold()
+	{
+		return _style == Style.BOLD || _style == Style.BOLD_ITALIC;
+	}
+
+	/** Returns true if this font has italic styling. */
+	public boolean isItalic()
+	{
+		return _style == Style.ITALIC || _style == Style.BOLD_ITALIC;
+	}
+
+	/** Returns true if this font has no bold/italic styling. */
+	public boolean isPlain()
+	{
+		return _style == Style.PLAIN;
+	}
+	
+	/** Removes bold styling if this font has it, or adds if if not. */
+	public void toggleBold()
+	{
+		switch (_style) {
+		case PLAIN:
+			_style = Style.BOLD;
+			break;
+		case BOLD:
+			_style = Style.PLAIN;
+			break;
+		case ITALIC:
+			_style = Style.BOLD_ITALIC;
+			break;
+		case BOLD_ITALIC:
+			_style = Style.ITALIC;
+			break;
+		}
+	}
+
+	/** Removes italic styling if this font has it, or adds if if not. */
+	public void toggleItalic()
+	{
+		switch (_style) {
+		case PLAIN:
+			_style = Style.ITALIC;
+			break;
+		case BOLD:
+			_style = Style.BOLD_ITALIC;
+			break;
+		case ITALIC:
+			_style = Style.PLAIN;
+			break;
+		case BOLD_ITALIC:
+			_style = Style.BOLD;
+			break;
+		}
+	}
+	
+	/** Sets the size of this font. */
+	public void setSize(int size)
+	{
+		_size = size;
+	}
+
+	/** Gets the size of this font. */
+	public int getSize()
+	{
+		return _size;
+	}
+
+	/** Sets the spacing of this font. */
+	public void setSpacing(float spacing)
+	{
+		_spacing = spacing;
+	}
+
+	/** Gets the spacing of this font. */
+	public float getSpacing()
+	{
+		return _spacing;
+	}
+	
+	@Override
+	public Font clone()
+	{
+		return new Font(this);
+	}
+
+	// Automatically generated.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((_familyName == null) ? 0 : _familyName.hashCode());
+		result = prime * result + _size;
+		result = prime * result + Float.floatToIntBits(_spacing);
+		result = prime * result + ((_style == null) ? 0 : _style.hashCode());
+		return result;
+	}
+
+	// Automatically generated.
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Font other = (Font) obj;
+		if (_familyName == null) {
+			if (other._familyName != null)
+				return false;
+		} else if (!_familyName.equals(other._familyName))
+			return false;
+		if (_size != other._size)
+			return false;
+		if (Float.floatToIntBits(_spacing) != Float.floatToIntBits(other._spacing))
+			return false;
+		if (_style != other._style)
+			return false;
+		return true;
+	}
+}
Index: /trunk/src/org/expeditee/core/GradientFill.java
===================================================================
--- /trunk/src/org/expeditee/core/GradientFill.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/GradientFill.java	(revision 1097)
@@ -0,0 +1,103 @@
+package org.expeditee.core;
+
+/**
+ * Represents a fill where the colour is blended from one colour at one point
+ * to another colour at another point.
+ * 
+ * @author cts16
+ */
+public class GradientFill extends Fill {
+
+	// Fill._colour is treated as the _fromColour (colour at the first point)
+	/** The colour at the second point. */
+	private Colour _toColour;
+	/** The point where the fill should be the first colour. */
+	private Point _fromPoint;
+	/** The point where the fill should be the second colour. */
+	private Point _toPoint;
+	/** Whether the gradient repeats at points not between the two specified points. */
+	private boolean _acyclic;
+	
+	/** Standard constructor. */
+	public GradientFill(Colour fromColour, Point fromPoint, Colour toColour, Point toPoint, boolean acyclic)
+	{
+		// Make sure private members are never null
+		if (fromColour == null) fromColour = new Colour();
+		if (fromPoint == null) fromPoint = new Point();
+		if (toColour == null) toColour = new Colour();
+		if (toPoint == null) toPoint = new Point();
+		
+		setFromColour(fromColour);
+		setFromPoint(fromPoint);
+		setToColour(toColour);
+		setToPoint(toPoint);
+		setAcyclic(acyclic);
+	}
+	
+	/** If acyclic is unspecified, defaults to true. */
+	public GradientFill(Colour fromColour, Point fromPoint, Colour toColour, Point toPoint)
+	{
+		this(fromColour,  fromPoint, toColour, toPoint, true);
+	}
+	
+	/** Sets the colour to blend away from. */
+	public void setFromColour(Colour colour)
+	{
+		setColour(colour);
+	}
+	
+	/** Gets the colour being blended away from. */
+	public Colour getFromColour()
+	{
+		return getColour();
+	}
+	
+	/** Sets the point to blend away from. */
+	public void setFromPoint(Point point)
+	{
+		_fromPoint = point;
+	}
+
+	/** Gets the point being blended away from. */
+	public Point getFromPoint()
+	{
+		return _fromPoint;
+	}
+	
+	/** Sets the colour to blend towards. */
+	public void setToColour(Colour colour)
+	{
+		_toColour = colour;
+	}
+
+	/** Gets the colour being blended toward. */
+	public Colour getToColour()
+	{
+		return _toColour;
+	}
+
+	/** Sets the point to blend towards. */
+	public void setToPoint(Point point)
+	{
+		_toPoint = point;
+	}
+
+	/** Gets the point being blended toward. */
+	public Point getToPoint()
+	{
+		return _toPoint;
+	}
+	
+	/** Sets if this fill should continue blending outside the given two points. */
+	public void setAcyclic(boolean acyclic)
+	{
+		_acyclic = acyclic;
+	}
+
+	/** Gets whether this fill continues blending outside the given two points. */
+	public boolean isAcyclic()
+	{
+		return _acyclic;
+	}
+
+}
Index: /trunk/src/org/expeditee/core/Image.java
===================================================================
--- /trunk/src/org/expeditee/core/Image.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Image.java	(revision 1097)
@@ -0,0 +1,233 @@
+package org.expeditee.core;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.gio.ImageManager;
+
+import com.sun.pdfview.PDFPage;
+
+/**
+ * A handle to an internal-type image maintained by the ImageManager.
+ * 
+ * @author cts16
+ */
+public class Image implements Serializable {
+
+	/** Auto-generated serialisation ID#; */
+	private static final long serialVersionUID = -5716308599893528323L;
+	
+	/** Integer representing an invalid size for width or height. */
+	public static final int INVALID_SIZE = -1;
+
+	/** The ImageManager used to perform platform-specific image interactions. */
+	private static ImageManager _manager = null;
+	
+	/** Sets the platform-specific ImageManager for all images. Can only be set once. */
+	public static void setManager(ImageManager manager)
+	{
+		if (_manager == null) _manager = manager;
+	}
+	
+	/** Gets a reference to the designated platform-specific image manager. */
+	public static ImageManager getManager()
+	{
+		return _manager;
+	}
+	
+	/** The handle ID# to use for the next-created image. */
+	private static long nextHandle = 1;
+	
+	/** The handle ID# for this image. */
+	private long _handle;
+	
+	/** Whether this image has a corresponding file on disk */
+	private boolean _isStoredOnDisk;
+	
+	/** Whether or not this image is animated (e.g. a GIF). */
+	private boolean _animated = false;
+	
+	/** The width of this image. */
+	private int _width = INVALID_SIZE;
+	/** The height of this image. */
+	private int _height = INVALID_SIZE;
+	
+	/**
+	 * Controls creation of image handles. Will only return null until an ImageManager is provided
+	 * with Image.setManager(ImageManager). This should only be used by the ImageManager to
+	 * create a handle to associate with a real internal image. Handles created by anyone else
+	 * will not be accepted by the image manager.
+	 */
+	public static Image get(boolean fromDisk)
+	{
+		// Return null until an image manager is available
+		if (_manager == null) return null;
+		
+		return new Image(fromDisk);
+	}
+	
+	/** Private constructor as instantiation handled through Image.get(boolean). */
+	private Image(boolean fromDisk)
+	{
+		// Assign the next available handle ID number
+		_handle = nextHandle;
+		nextHandle++;
+		
+		// Remember if this image is stored on disk or not
+		_isStoredOnDisk = fromDisk;
+	}
+	
+	/** Returns the ID# for this handle. */
+	public long getHandle()
+	{
+		return _handle;
+	}
+	
+	/** Returns whether or not this image is stored on disk. */
+	public boolean isStoredOnDisk()
+	{
+		return _isStoredOnDisk;
+	}
+	
+	/** Sets the animated property of this image. */
+	public void setIsAnimated(boolean isAnimated)
+	{
+		_animated = isAnimated;
+	}
+	
+	/** Returns whether this image is animated. */
+	public boolean isAnimated()
+	{
+		return _animated;
+	}
+	
+	/** Convenience access to ImageManager.createImage(Dimension). */
+	public static Image createImage(Dimension size)
+	{
+		return _manager.createImage(size);
+	}
+	
+	/** Convenience access to ImageManager.createImage(int, int). */
+	public static Image createImage(int width, int height)
+	{
+		return _manager.createImage(width, height);
+	}
+	
+	/** Convenience access to ImageManager.createImage(Dimension, boolean). */
+	public static Image createImage(Dimension size, boolean hintUseAcceleratedMemory)
+	{
+		return _manager.createImage(size, hintUseAcceleratedMemory);
+	}
+	
+	/** Convenience access to ImageManager.createImage(int, int, boolean). */
+	public static Image createImage(int width, int height, boolean hintUseAcceleratedMemory)
+	{
+		return _manager.createImage(width, height, hintUseAcceleratedMemory);
+	}
+	/** Convenience access to ImageManager.createImage(int, int, int[]). */
+	public static Image createImage(int width, int height, int[] pixelData)
+	{
+		return _manager.createImage(width, height, pixelData);
+	}
+	
+	/** Convenience access to ImageManager.createImage(int, int, PDFPage). */
+	public static Image createImage(int width, int height, PDFPage page)
+	{
+		return _manager.createImage(width, height, page);
+	}
+	
+	/** Convenience access to ImageManager.createImageAsCroppedCopy(Image, int, int, int, int). */
+	public static Image createImageAsCroppedCopy(Image orig, int x, int y, int width, int height)
+	{
+		return _manager.createImageAsCroppedCopy(orig, x,  y, width, height);
+	}
+	
+	/** Convenience access to ImageManager.getImage(URL). */
+	public static Image getImage(URL url)
+	{
+		return _manager.getImage(url);
+	}
+
+	/** Convenience access to ImageManager.getImage(String). */
+	public static Image getImage(String filename)
+	{
+		return _manager.getImage(filename);
+	}
+
+	/** Convenience access to ImageManager.getImage(HttpURLConnection). */
+	public static Image getImage(HttpURLConnection connection) throws IOException
+	{
+		return _manager.getImage(connection);
+	}
+
+	/** Convenience access to ImageManager.releaseImage(Image). */
+	public void releaseImage()
+	{
+		_manager.releaseImage(this);
+	}
+
+	/** Convenience access to ImageManager.isImageValid(Image). */
+	public boolean isValid()
+	{
+		return _manager.isImageValid(this);
+	}
+	
+	/** Gets the width of this image. */
+	public int getWidth()
+	{
+		// Cache the width of the image if its isn't already 
+		if (_width == INVALID_SIZE) _width = _manager.getWidth(this);
+		
+		return _width;
+	}
+	
+	/** Gets the height of this image. */
+	public int getHeight()
+	{
+		// Cache the height of the image if its isn't already 
+		if (_height == INVALID_SIZE) _height = _manager.getHeight(this);
+		
+		return _height;
+	}
+	
+	/** Gets the size of this image. */
+	public Dimension getSize()
+	{
+		return new Dimension(getWidth(), getHeight());
+	}
+	
+	/** Convenience access to ImageManager.getPixel(Image, int, int). */
+	public Colour getPixel(int x, int y)
+	{
+		return _manager.getPixel(this, x, y);
+	}
+
+	/** Convenience access to ImageManager.setPixel(Image, int, int, Colour). */
+	public void setPixel(int x, int y, Colour c)
+	{
+		_manager.setPixel(this, x, y, c);
+	}
+
+	/** Convenience access to ImageManager.writeToDisk(Image, String, File). */
+	public boolean writeToDisk(String format, File file) throws IOException
+	{
+		return _manager.writeImageToDisk(this, format, file);
+	}
+	
+	/** Returns the bounds of this image. */
+	public AxisAlignedBoxBounds getBounds()
+	{
+		return new AxisAlignedBoxBounds(0, 0, getWidth(), getHeight());
+	}
+	
+	@Override
+	protected void finalize()
+	{
+		// Tell the image manager that nobody's using this image anymore
+		releaseImage();
+	}
+}
Index: /trunk/src/org/expeditee/core/InOutReference.java
===================================================================
--- /trunk/src/org/expeditee/core/InOutReference.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/InOutReference.java	(revision 1097)
@@ -0,0 +1,58 @@
+package org.expeditee.core;
+
+/**
+ * A wrapper type to allow the ability to pass values by reference.
+ * 
+ * @author cts16
+ */
+public class InOutReference<T> {
+
+	/** The value of this reference. */
+	private T _value;
+	
+	/** Default constructor. */
+	public InOutReference()
+	{
+		_value = null;
+	}
+	
+	/** Standard constructor. */
+	public InOutReference(T value)
+	{
+		_value = value;
+	}
+	
+	/** Copy constructor. */
+	public InOutReference(InOutReference<T> other)
+	{
+		this(other._value);
+	}
+	
+	/** Gets the value of this reference (dereferences). */
+	public T get()
+	{
+		return _value;
+	}
+	
+	/** Sets the value of this reference. */
+	public void set(T value)
+	{
+		_value = value;
+	}
+	
+	@Override
+	public InOutReference<T> clone()
+	{
+		return new InOutReference<T>(this);
+	}
+	
+	@Override
+	public boolean equals(Object other)
+	{
+		if (other instanceof InOutReference<?>) {
+			return ((InOutReference<?>) other)._value.equals(_value);
+		}
+		
+		return false;
+	}
+}
Index: /trunk/src/org/expeditee/core/Lifetime.java
===================================================================
--- /trunk/src/org/expeditee/core/Lifetime.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Lifetime.java	(revision 1097)
@@ -0,0 +1,106 @@
+package org.expeditee.core;
+
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.TimeoutQueue.TimeoutHandle;
+import org.expeditee.gio.input.InputEvent;
+import org.expeditee.gio.input.InputEvent.InputType;
+import org.expeditee.gio.input.InputEventListener;
+import org.expeditee.gio.input.TimeoutInputEvent;
+
+public class Lifetime
+{
+	/** Value representing an infinitely-long popup lifetime. */
+	public static final long INFINITE_LIFETIME = -1;
+	
+	/** The length of this lifetime. */
+	private long _lifetime;
+	/** The time as returned by System.currentTimeMillis() when this lifetime will expire. */
+	private long _deathTime;
+	
+	private TimeoutHandle _expiryHandle = null;
+	private Runnable _expiryRoutine = null;
+	private InputEventListener _expiryListener = new InputEventListener()
+	{
+		@Override
+		public boolean onInputEvent(InputEvent event)
+		{
+			if (event.getInputType() == InputType.TIMEOUT) {
+				TimeoutInputEvent timeoutEvent = (TimeoutInputEvent) event;
+				if (timeoutEvent.getHandle() == _expiryHandle) {
+					_expiryRoutine.run();
+					cancelExpiry();
+				}
+			}
+			return false;
+		}
+	};
+	
+	public Lifetime(long lifetime)
+	{
+		if (lifetime < 0) lifetime = INFINITE_LIFETIME;
+		_lifetime = lifetime;
+		refresh();
+	}
+	
+	public void setLifetime(long lifetime)
+	{
+		if (lifetime < 0) lifetime = INFINITE_LIFETIME;
+		
+		if (_lifetime != INFINITE_LIFETIME) _deathTime -= _lifetime;
+		
+		_lifetime = lifetime;
+		
+		if (_lifetime != INFINITE_LIFETIME) _deathTime += _lifetime;
+	}
+	
+	public void refresh()
+	{
+		if (_lifetime == INFINITE_LIFETIME) {
+			_deathTime = System.currentTimeMillis();
+		} else {
+			_deathTime = System.currentTimeMillis() + _lifetime;
+		}
+	}
+	
+	public long remainingLifetime()
+	{
+		if (_lifetime == INFINITE_LIFETIME) return INFINITE_LIFETIME;
+		
+		long remaining = _deathTime - System.currentTimeMillis();
+		
+		if (remaining < 0) remaining = 0;
+		
+		return remaining;
+	}
+	
+	public boolean hasExpired()
+	{
+		return remainingLifetime() == 0;
+	}
+	
+	public void cancelExpiry()
+	{
+		if (_expiryHandle != null) {
+			EcosystemManager.getInputManager().cancelTimeout(_expiryHandle);
+			EcosystemManager.getInputManager().removeInputEventListener(_expiryListener);
+			_expiryHandle = null;
+			_expiryRoutine = null;
+		}
+	}
+	
+	public void onExpiry(Runnable routine)
+	{	
+		if (_expiryHandle != null) {
+			cancelExpiry();
+		}
+		
+		if (routine == null || _lifetime == INFINITE_LIFETIME) return;
+		
+		_expiryRoutine = routine;
+		
+		EcosystemManager.getInputManager().registerInputEventListener(_expiryListener);
+		
+		_expiryHandle = EcosystemManager.getInputManager().addTimeout((int) remainingLifetime());
+	}
+
+}
Index: /trunk/src/org/expeditee/core/Line.java
===================================================================
--- /trunk/src/org/expeditee/core/Line.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Line.java	(revision 1097)
@@ -0,0 +1,180 @@
+package org.expeditee.core;
+
+/**
+ * The line class represents a line-segment in 2-dimensional space (i.e.
+ * the connection between 2 points).
+ * 
+ * @author cts16
+ */
+public class Line {
+	
+	/** The amount of error to use to ensure lines passing through pixels are hit.
+	    Equal to 1 / sqrt(2). */
+	public static final double PIXEL_ERROR = 0.707106781;
+	
+	/** The two end-points of the line segment. */
+	private Point _p1, _p2;
+	
+	public Line(Point p1, Point p2)
+	{
+		_p1 = new Point(p1);
+		_p2 = new Point(p2);
+	}
+	
+	public Line(int x1, int y1, int x2, int y2)
+	{
+		_p1 = new Point(x1, y1);
+		_p2 = new Point(x2, y2);
+	}
+	
+	/** Gets the position of the first end of the line. */
+	public Point getFirstEnd()
+	{
+		return _p1;
+	}
+	
+	/** Gets the position of the second end of the line. */
+	public Point getSecondEnd()
+	{
+		return _p2;
+	}
+	
+	/** Sets the first end of the line to the given point. */
+	public void setFirstEnd(Point p)
+	{
+		if (p != null) _p1 = p;
+	}
+	
+	/** Sets the second end of the line to the given point. */
+	public void setSecondEnd(Point p)
+	{
+		if (p != null) _p2 = p;
+	}
+	
+	/** Gets the change in x-coordinate from one end to the other. */
+	public int deltaX()
+	{
+		return _p2.x - _p1.x;
+	}
+	
+	/** Gets the change in y-coordinate from one end to the other. */
+	public int deltaY()
+	{
+		return _p2.y - _p1.y;
+	}
+	
+	/** Gets the length of the line. */
+	public double length()
+	{
+		return Point.distanceBetween(_p1, _p2);
+	}
+	
+	/**
+	 * Checks if this line intersects the given line.
+	 * 
+	 * https://stackoverflow.com/questions/563198/whats-the-most-efficent-way-to-calculate-where-two-line-segments-intersect
+	 */
+	public boolean intersects(Line other, double error)
+	{
+		Point p = this._p1;
+		Point q = other._p1;
+		Point r = Point.difference(this._p2, p);
+		Point s = Point.difference(other._p2, q);
+		
+		int numerator = crossLength(Point.difference(q, p), r);
+		int denominator = crossLength(r, s);
+		
+		if (denominator == 0) {
+			return numerator == 0 && (contains(other._p1, error) || contains(other._p2, error));
+		} else {
+			double u = ((double) numerator) / denominator;
+			return u >= 0 && u <= 1 && this.contains(new Point((int) (q.x + u * s.x), (int) (q.y + u * s.y)), error);
+		}
+	}
+	
+	/**
+	 * Calculates the length of the cross-product between points a and b.
+	 */
+	private int crossLength(Point a, Point b)
+	{
+		return a.x * b.y - a.y * b.x;
+	}
+	
+	/**
+	 * Checks if the given point falls on the line.
+	 * 
+	 * @param p
+	 * 		The point to check.
+	 * 
+	 * @param error
+	 * 		The maximum distance from the line that will be considered part of the line.
+	 * 
+	 * @return
+	 * 		True if the given point falls on the line (within <code>error</code>), false if not.
+	 */
+	public boolean contains(Point p, double error)
+	{
+		// If the point is within error from either end,
+		// then we say it's on the line
+		if (Point.distanceBetween(p, _p1) <= error || Point.distanceBetween(p, _p2) <= error) return true;
+		
+		// Point's projection onto the line must be between the two ends
+		double length = length();
+		double projection = Point.dotProduct(_p1, _p2, p) / length;
+		if (!(0 <= projection && projection <= length)) return false;
+		
+		// Point must be within error distance from the line
+		double hypotenuse = Point.distanceBetween(p, _p1);
+		if ((hypotenuse * hypotenuse - projection * projection) <= (error * error)) return true;
+		
+		// The point is too far from the line
+		return false;
+	}
+	
+	/**
+	 * Checks if there are any intersections between to sets of lines.
+	 * Does not check for intersections within each set.
+	 * 
+	 * @param lines1
+	 * 		The first set of lines.
+	 * 
+	 * @param lines2
+	 * 		The second set of lines.
+	 * 
+	 * @return
+	 * 		True if a line from set 1 intersects a line from set 2,
+	 * 		false if no intersections occur.
+	 */
+	public static boolean anyIntersections(Line[] lines1, Line[] lines2, double error)
+	{
+		for (Line line1 : lines1) {
+			for (Line line2 : lines2) {
+				if (line1.intersects(line2, error)) return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	/** Gets the point on the line nearest to the given point. */
+	public Point getPointNearestTo(Point p)
+	{
+		// If the line is degenerate, then that is the nearest point
+		if (_p1.equals(_p2)) return _p1.clone();
+		
+		// Work out the distance from _p1 to the nearest point
+		double lineLength = length();
+		double projectionLength = Point.dotProduct(_p1, _p2, p) / lineLength;
+		
+		// Calculate the position of the nearest point
+		if (projectionLength <= 0.0) {
+			return _p1.clone();
+		} else if (projectionLength >= lineLength) {
+			return _p2.clone();
+		} else {
+			double scaleFactor = projectionLength / lineLength;
+			return _p1.clone().add((_p2.x - _p1.x) * scaleFactor, (_p2.y - _p1.y) * scaleFactor);
+		}
+	}
+
+}
Index: /trunk/src/org/expeditee/core/Paintable.java
===================================================================
--- /trunk/src/org/expeditee/core/Paintable.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Paintable.java	(revision 1097)
@@ -0,0 +1,9 @@
+package org.expeditee.core;
+
+public interface Paintable
+{
+	public void paint();
+	
+	public void getPaintBounds();
+
+}
Index: /trunk/src/org/expeditee/core/Point.java
===================================================================
--- /trunk/src/org/expeditee/core/Point.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Point.java	(revision 1097)
@@ -0,0 +1,204 @@
+package org.expeditee.core;
+
+/**
+ * Represents a (x,y)-coordinate in 2-dimensional space.
+ * Coordinates are integral.
+ * 
+ * @author cts16
+ */
+public class Point {
+	
+	/** The origin of the coordinate system. */
+	public static final Point ORIGIN = new Point(0, 0);
+	
+	/** The x-coordinate of this point. */
+	public int x;
+	/** The y-coordinate of this point. */
+	public int y;
+	
+	/** Default constructor creates a point at the origin. */
+	public Point()
+	{
+		this(0, 0);
+	}
+	
+	/** Standard constructor. */
+	public Point(int x, int y)
+	{
+		set(x, y);
+	}
+	
+	/** Convenience constructor automatically casts floats to ints. */
+	public Point(float x, float y)
+	{
+		this((int) x, (int) y);
+	}
+	
+	/** Copy constructor. */
+	public Point(Point other)
+	{
+		this();
+		set(other);
+	}
+	
+	/** Sets the position of the point. */
+	public Point set(int x, int y)
+	{
+		this.x = x;
+		this.y = y;
+		return this;
+	}
+	
+	/** Sets the position of this point to be the same as the given point.
+	    If p is null, no change is made. */
+	public Point set(Point p)
+	{
+		if (p != null) set(p.x, p.y);
+		return this;
+	}
+	
+	/** Moves the point by the given amount in each dimension. */
+	public Point add(int x, int y)
+	{
+		this.x += x;
+		this.y += y;
+		return this;
+	}
+	
+	/** Moves the point by the given amount in each dimension.
+	    Doubles are automatically cast to ints. */
+	public Point add(double x, double y)
+	{
+		return add((int) x, (int) y);
+	}
+	
+	/** Adds the coordinates of the given point to this point's coordinates
+	    (treating points as vectors). */
+	public Point add(Point p)
+	{
+		 if (p != null) add(p.x, p.y);
+		 return this;
+	}
+	
+	/**
+	 * Rotates this point around another point.
+	 * 
+	 * @param angle
+	 * 		The angle to rotate through (clockwise, in radians).
+	 * 
+	 * @param x
+	 * 		The x-coordinate of the centre of rotation.
+	 * 
+	 * @param y
+	 * 		The y-coordinate of the centre of rotation.
+	 */
+	public Point rotate(double angle, int x, int y)
+	{
+		// Find the distance from this point to the centre of rotation
+		// (should stay the same during rotation)
+		double length = Point.distanceBetween(x, y, this.x, this.y);
+		
+		// Find the starting elevation angle of this point relative to
+		// the centre of rotation.
+		double startingAngle = Math.atan2(this.y - y, this.x - x);
+		
+		// Increase/decrease the elevation angle by the given amount 
+		double finalAngle = startingAngle + angle;
+		
+		// Calculate the point at the same distance from the centre of rotation
+		// elevated at the new angle.
+		this.x = (int) (x + length * Math.cos(finalAngle));
+		this.y = (int) (y + length * Math.sin(finalAngle));
+		
+		return this;
+	}
+	
+	/**
+	 * Rotates this point around another point.
+	 * 
+	 * @param angle
+	 * 		The angle to rotate through (clockwise, in radians).
+	 * 
+	 * @param cor
+	 * 		The centre of rotation.
+	 */
+	public Point rotate(double angle, Point cor)
+	{
+		if (cor != null) rotate(angle, cor.x, cor.y);
+		return this;
+	}
+	
+	/** Gets the distance from this point to another point. */
+	public double getDistanceTo(Point other)
+	{
+		return distanceBetween(this, other);
+	}
+	
+	/** Gets the distance between two points. */
+	public static double distanceBetween(Point p1, Point p2)
+	{
+		if (p1 == null || p2 == null) return 0.0;
+		
+		return distanceBetween(p1.x, p1.y, p2.x, p2.y);
+	}
+
+	/** Gets the distance between two points specified by their coordinates. */
+	public static double distanceBetween(int x1, int y1, int x2, int y2)
+	{
+		double x = x1 - x2;
+		double y = y1 - y2;
+		return Math.sqrt(x * x + y * y);
+	}
+	
+	@Override
+	public boolean equals(Object other)
+	{
+		if (other instanceof Point) {
+			Point p = (Point) other;
+			return this.x == p.x && this.y == p.y;
+		}
+		return false;
+	}
+
+	/** Gets the x-coordinate of this point. */
+	public int getX()
+	{
+		return x;
+	}
+	
+	/** Gets the y-coordinate of this point. */
+	public int getY()
+	{
+		return y;
+	}
+	
+	@Override
+	public Point clone()
+	{
+		return new Point(this);
+	}
+	
+	/** Gets a point representing the difference between the two given points
+	    (returned point should be treated as a vector). */
+	public static Point difference(Point p1, Point p2)
+	{
+		if (p1 == null || p2 == null) return null;
+		return new Point(p1.x - p2.x, p1.y - p2.y);
+	}
+	
+	/** Calculates the dot-product between two lines that share a common point. */
+	public static double dotProduct(Point commonPoint, Point lineEnd1, Point lineEnd2)
+	{
+		if (commonPoint == null || lineEnd1 == null || lineEnd2 == null) return 0.0;
+		
+		Point p1 = new Point(lineEnd1.x - commonPoint.x, lineEnd1.y - commonPoint.y);
+		Point p2 = new Point(lineEnd2.x - commonPoint.x, lineEnd2.y - commonPoint.y);
+		return p1.x * p2.x + p1.y * p2.y;
+	}
+	
+	@Override
+	public String toString()
+	{
+		return "(" + x + ", " + y + ")";
+	}
+}
Index: /trunk/src/org/expeditee/core/Range.java
===================================================================
--- /trunk/src/org/expeditee/core/Range.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Range.java	(revision 1097)
@@ -0,0 +1,100 @@
+package org.expeditee.core;
+
+/**
+ * Represents a range of ordered values defined by a start and end value.
+ * Range can be open or closed at each end (i.e. the start and end points
+ * can be considered inside or outside the range). Either/both ends can
+ * also be null representing no lower/upper bound. If the upper bound is
+ * lower than the lower bound, the range is an outside range (where points
+ * outside the two bounds are considered contained by the range).
+ * 
+ * @author cts16
+ *
+ * @param <T> The type of values in this range. Must have an order and
+ * 				therefore must implement Comparable. 
+ */
+public class Range<T extends Comparable<T>> {
+
+	/** The lowest value in the range. If null, then the range has no lower bound. */
+	public T lowerBound;
+	/** The highest value in the range. If null, then the range has no upper bound. */
+	public T upperBound;
+	/** Whether the lowest value is open or closed (outside or inside the range respectively). */
+	public boolean lowerBoundClosed;
+	/** Whether the end value is open or closed. */
+	public boolean upperBoundClosed;
+	
+	/** Standard constructor considers the lower bound closed and upper bound open. */
+	public Range(T lowerBound, T upperBound)
+	{
+		this(lowerBound, upperBound, true, false);
+	}
+	
+	/** Standard constructor. */
+	public Range(T lowerBound, T upperBound, boolean lowerBoundClosed, boolean upperBoundClosed)
+	{
+		this.lowerBound = lowerBound;
+		this.upperBound = upperBound;
+		this.lowerBoundClosed = lowerBoundClosed;
+		this.upperBoundClosed = upperBoundClosed;
+	}
+	
+	/** Whether this range contains the given value. */
+	public boolean contains(T t)
+	{
+		// A null value is never inside the bound
+		if (t == null) return false;
+		
+		// See how the value compares to the two boundaries
+		int compStart = lowerBound != null ? t.compareTo(lowerBound) : 0;
+		int compEnd = upperBound != null ? t.compareTo(upperBound) : 0;
+		
+		// See if this is an outside range
+		boolean outsideRange = lowerBound != null && upperBound != null && upperBound.compareTo(lowerBound) < 0;
+		
+		// Make sure it's above the lower bound
+		boolean aboveLowerBound = 	(lowerBound == null) || // No lower bound, or...
+									(compStart > 0) || // Strictly above the lower bound, or...
+									(compStart == 0 && lowerBoundClosed); // On the lower bound but it's closed
+		
+		// Make sure it's below the upper bound
+		boolean belowUpperBound = 	(upperBound == null) || // No upper bound, or...
+									(compEnd < 0) || // Strictly below the upper bound, or...
+									(compEnd == 0 && upperBoundClosed); // On the upper bound but it's closed
+		
+		// In an inside range if we are below the upper bound and above the lower bound,
+		// in an outside range if we are below the upper bound or above the lower bound
+		return 	(aboveLowerBound && belowUpperBound) ||
+				(outsideRange && (aboveLowerBound || belowUpperBound));
+	}
+	
+	/** Creates an inside range when the order of the two ends is unknown. Both ends are considered closed. */
+	public static <T extends Comparable<T>> Range<T> insideRangeFromUncertainEnds(T anEnd, T anotherEnd)
+	{
+		return insideRangeFromUncertainEnds(anEnd, anotherEnd, true, true);
+	}
+	
+	/** Creates an inside range when the order of the two ends is unknown. */
+	public static <T extends Comparable<T>> Range<T> insideRangeFromUncertainEnds(T anEnd, T anotherEnd, boolean anEndClosed, boolean anotherEndClosed)
+	{
+		// If both ends are null then ordering doesn't matter
+		if (anEnd == null && anotherEnd == null) return new Range<T>(anEnd, anotherEnd, anEndClosed, anotherEndClosed);
+		
+		// Can't determine order of a single end
+		if (anEnd == null || anotherEnd == null) return null;
+		
+		// Put the two ends in order (so anEnd is lower than anotherEnd),
+		// keeping the respective closed flags with their ends
+		if (anEnd.compareTo(anotherEnd) > 0) {
+			T temp = anEnd;
+			anEnd = anotherEnd;
+			anotherEnd = temp;
+			boolean bTemp = anEndClosed;
+			anEndClosed = anotherEndClosed;
+			anotherEndClosed = bTemp;
+		}
+		
+		// Create a range from the reorder ends
+		return new Range<T>(anEnd, anotherEnd, anEndClosed, anotherEndClosed);
+	}
+}
Index: /trunk/src/org/expeditee/core/Stroke.java
===================================================================
--- /trunk/src/org/expeditee/core/Stroke.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/Stroke.java	(revision 1097)
@@ -0,0 +1,67 @@
+package org.expeditee.core;
+
+/**
+ * Represents how lines/polylines are rendered.
+ *  
+ * @author cts16
+ */
+public class Stroke {
+
+	/** The dash array to use for a solid line. */
+	public static final float[] SOLID_DASH_ARRAY = new float[] { 1.0f, 0.0f };
+	
+	/** The methods of rendering the open end of a line. */
+	public enum CAP {
+		BUTT, // Hard squared-off cap at the logical end of the line
+		ROUND, // Semi-circular cap with radius equal to the line thickness
+		SQUARE // Similar to BUTT but extends from the logical end of the line by half the line thickness
+	}
+	
+	/** The methods of rendering the joining point between two segments of a polyline. */
+	public enum JOIN {
+		BEVEL, // Corners of line are connected
+		MITER, // Edges of line extend to meet each other
+		ROUND // Joins are rendered as circles
+	}
+	
+	/** The thickness of the line. */
+	public float thickness;
+	/** The method for rendering line ends. */
+	public CAP capType;
+	/** The method for rendering line joins. */
+	public JOIN joinType;
+	/** An array of distances for on- and off-strokes when rendering dashed lines. */
+	public float[] dashArray;
+	/** Where to start in the dash array. */
+	public float dashPhase;
+	
+	/** Default constructor. */
+	public Stroke()
+	{
+		this(1.0f);
+	}
+	
+	/** Standard constructor. Defaults to butt caps, bevel joins and a solid line. */
+	public Stroke(float thickness)
+	{
+		this(thickness, CAP.BUTT, JOIN.BEVEL);
+	}
+	
+	/** Standard constructor. Defaults to a solid line. */
+	public Stroke(float thickness, CAP capType, JOIN joinType)
+	{
+		this(thickness, capType, joinType, SOLID_DASH_ARRAY, 0.0f);
+	}
+	
+	/** Standard constructor. */
+	public Stroke(float thickness, CAP capType, JOIN joinType, float[] dashArray, float dashPhase)
+	{
+		this.thickness = thickness;
+		this.capType = capType;
+		this.joinType = joinType;
+		this.dashArray = dashArray;
+		this.dashPhase = dashPhase;
+	}
+	
+	
+}
Index: /trunk/src/org/expeditee/core/StrongReference.java
===================================================================
--- /trunk/src/org/expeditee/core/StrongReference.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/StrongReference.java	(revision 1097)
@@ -0,0 +1,27 @@
+package org.expeditee.core;
+
+public class StrongReference<T> {
+
+	private T _referend;
+	
+	public StrongReference(T referend)
+	{
+		set(referend);
+	}
+	
+	public StrongReference()
+	{
+		this(null);
+	}
+	
+	public T get()
+	{
+		return _referend;
+	}
+	
+	public void set(T referend)
+	{
+		_referend = referend;
+	}
+	
+}
Index: /trunk/src/org/expeditee/core/TextHitInfo.java
===================================================================
--- /trunk/src/org/expeditee/core/TextHitInfo.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/TextHitInfo.java	(revision 1097)
@@ -0,0 +1,21 @@
+package org.expeditee.core;
+
+public class TextHitInfo {
+
+	public int charIndex;
+	public boolean isLeadingEdge;
+	
+	public TextHitInfo(int charIndex, boolean isLeadingEdge)
+	{
+		this.charIndex = charIndex;
+		this.isLeadingEdge = isLeadingEdge;
+	}
+	
+	public int getInsertionIndex() {
+        return isLeadingEdge ? charIndex : charIndex + 1;
+    }
+	
+	public int getCharIndex() {
+        return charIndex;
+    }
+}
Index: /trunk/src/org/expeditee/core/TextLayout.java
===================================================================
--- /trunk/src/org/expeditee/core/TextLayout.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/TextLayout.java	(revision 1097)
@@ -0,0 +1,153 @@
+package org.expeditee.core;
+
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.gio.TextLayoutManager;
+
+/**
+ * Purely a handle type for the TextLayoutManager.
+ * 
+ * @author cts16
+ *
+ */
+public class TextLayout {
+
+	private static TextLayoutManager _manager;
+	
+	public static void setManager(TextLayoutManager manager)
+	{
+		if (_manager == null) _manager = manager;
+	}
+	
+	public static TextLayoutManager getManager()
+	{
+		return _manager;
+	}
+	
+	private static long nextHandle = 1;
+	
+	private long _handle;
+	
+	private String _line;
+	private Font _font;
+	private int _startCharIndex;
+	private int _endCharIndex;
+	
+	public static TextLayout get(String line, Font font, int startCharIndex, int endCharIndex)
+	{
+		if (_manager == null) return null;
+		
+		return new TextLayout(line, font, startCharIndex, endCharIndex);
+	}
+	
+	private TextLayout(String line, Font font, int startCharIndex, int endCharIndex)
+	{
+		_handle = nextHandle;
+		nextHandle++;
+		
+		_line = new String(line);
+		_font = new Font(font);
+		_startCharIndex = startCharIndex;
+		_endCharIndex = endCharIndex;
+	}
+	
+	public long getHandle()
+	{
+		return _handle;
+	}
+	
+	public float getAdvance()
+	{
+		return _manager.getAdvance(this);
+	}
+	
+	public int getCharacterCount()
+	{
+		return _manager.getCharacterCount(this);
+	}
+	
+	public TextHitInfo getNextLeftHit(int offset)
+	{
+		return _manager.getNextLeftHit(this, offset);
+	}
+	
+	public TextHitInfo getNextLeftHit(TextHitInfo hit)
+	{
+		return _manager.getNextLeftHit(this, hit);
+	}
+	
+	public TextHitInfo getNextRightHit(int offset)
+	{
+		return _manager.getNextRightHit(this, offset);
+	}
+	
+	public TextHitInfo getNextRightHit(TextHitInfo hit)
+	{
+		return _manager.getNextRightHit(this, hit);
+	}
+	
+	public float[] getCaretInfo(TextHitInfo hit)
+	{
+		return _manager.getCaretInfo(this, hit);
+	}
+	
+	public TextHitInfo hitTestChar(float x, float y)
+	{
+		return _manager.hitTestChar(this, x, y);
+	}
+	
+	public AxisAlignedBoxBounds getPixelBounds(float x, float y)
+	{
+		return _manager.getPixelBounds(this, x, y);
+	}
+	
+	public AxisAlignedBoxBounds getLogicalHighlightShape(int firstEndpoint, int secondEndpoint)
+	{
+		return _manager.getLogicalHighlightShape(this, firstEndpoint, secondEndpoint);
+	}
+	
+	public float getAscent()
+	{
+		return _manager.getAscent(this);
+	}
+	
+	public float getDescent()
+	{
+		return _manager.getDescent(this);
+	}
+	
+	public float getLeading()
+	{
+		return _manager.getLeading(this);
+	}
+	
+	public String getLine()
+	{
+		return new String(_line);
+	}
+	
+	public Font getFont()
+	{
+		return new Font(_font);
+	}
+	
+	public int getStartCharIndex()
+	{
+		return _startCharIndex;
+	}
+	
+	public int getEndCharIndex()
+	{
+		return _endCharIndex;
+	}
+	
+	public void release()
+	{
+		_manager.releaseLayout(this);
+	}
+	
+	@Override
+	protected void finalize()
+	{
+		release();
+	}
+}
Index: /trunk/src/org/expeditee/core/bounds/AxisAlignedBoxBounds.java
===================================================================
--- /trunk/src/org/expeditee/core/bounds/AxisAlignedBoxBounds.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/bounds/AxisAlignedBoxBounds.java	(revision 1097)
@@ -0,0 +1,368 @@
+package org.expeditee.core.bounds;
+
+import org.expeditee.Util;
+import org.expeditee.core.Dimension;
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+
+/**
+ * A bounds that represents a rectangular area where the left and right sides are parallel
+ * to the y-axis and the top and bottom sides are parallel to the x-axis.
+ * 
+ * @author cts16
+ */
+public class AxisAlignedBoxBounds extends Bounds {
+	
+	/** The top-left corner of the bounds. */
+	private Point _topLeft;
+	/** The width and height of the rectangular bounds. */
+	private Dimension _size;
+	
+	/** Creates an AxisAlignedBoxBounds at the given location with the given size. */
+	public AxisAlignedBoxBounds(Point topLeft, Dimension size)
+	{
+		setTopLeft(topLeft);
+		setSize(size);
+	}
+
+	/** Creates an AxisAlignedBoxBounds at the given location with the given size. */
+	public AxisAlignedBoxBounds(int x, int y, int width, int height)
+	{
+		this(new Point(x, y), new Dimension(width, height));
+	}
+
+	/** Creates an AxisAlignedBoxBounds with the same location and size as <code>other</code>. */
+	public AxisAlignedBoxBounds(AxisAlignedBoxBounds other)
+	{
+		if (other != null) {
+			setTopLeft(other._topLeft);
+			setSize(other._size);
+		} else {
+			setTopLeft(null);
+			setSize(null);
+		}
+	}
+	
+	/** Creates an AxisAlignedBoxBounds that completely encloses the given bounds. */
+	public static AxisAlignedBoxBounds getEnclosing(Bounds b)
+	{
+		if (b == null) return null;
+		
+		int maxX = b.getMaxX();
+		int minX = b.getMinX();
+		int maxY = b.getMaxY();
+		int minY = b.getMinY();
+		
+		Point topLeft = new Point(minX, minY);
+		Dimension size = new Dimension(maxX - minX + 1, maxY - minY + 1);
+		
+		return new AxisAlignedBoxBounds(topLeft, size);
+	}
+	
+	/** Gets the top-left corner position of this AxisAlignedBoxBounds. */
+	public Point getTopLeft()
+	{
+		return _topLeft;
+	}
+	
+	/** Gets the top-left corner position of this AxisAlignedBoxBounds. */
+	public Point getBottomRight()
+	{
+		return _topLeft.clone().add(_size.width - 1, _size.height - 1);
+	}
+	
+	/** Sets the top-left corner of this AxisAlignedBoxBounds to the given point. */
+	public void setTopLeft(Point topLeft)
+	{
+		if (topLeft == null) topLeft = new Point(0, 0);
+		this._topLeft = topLeft;
+	}
+	
+	/** Gets the size of this Bounds. */
+	public Dimension getSize()
+	{
+		return _size;
+	}
+	
+	/** Sets the size of this Bounds (null sets to 1x1). */
+	public void setSize(Dimension size)
+	{
+		if (size == null) size = new Dimension(1, 1);
+		this._size = size;
+	}
+	
+	/** Gets the width of this AxisAlignedBoxBounds. */
+	public int getWidth()
+	{
+		return _size.width;
+	}
+
+	/** Gets the height of this AxisAlignedBoxBounds. */
+	public int getHeight()
+	{
+		return _size.height;
+	}
+
+	/** Gets the x-coordinate of the centre of this AxisAlignedBoxBounds. */
+	public int getCentreX()
+	{
+		return _topLeft.x + _size.width / 2;
+	}
+
+	/** Gets the y-coordinate of the centre of this AxisAlignedBoxBounds. */
+	public int getCentreY()
+	{
+		return _topLeft.y + _size.height / 2;
+	}
+
+	/** Gets the coordinates of the centre of this AxisAlignedBoxBounds. */
+	public Point getCentre()
+	{
+		return new Point(getCentreX(), getCentreY());
+	}
+	
+	/**
+	 * Updates this Bounds so it covers both its original area and
+	 * the area of the given bounds (plus some possible extra area).
+	 * 
+	 * E.g.   /////////**********
+	 *        /       /         *
+	 *        / other /    \\\\\\
+	 *        /////////    \    \
+	 *        *            \this\
+	 *        *  result    \    \
+	 *        *************\\\\\\
+	 */
+	public AxisAlignedBoxBounds combineWith(AxisAlignedBoxBounds other)
+	{
+		// Can't combine with nothing
+		if (other == null) return this;
+		
+		int top = Math.min(this.getMinY(), other.getMinY());
+		int bottom = Math.max(this.getMaxY(), other.getMaxY());
+		int left = Math.min(this.getMinX(), other.getMinX());
+		int right = Math.max(this.getMaxX(), other.getMaxX());
+		
+		_topLeft.set(left, top);
+		_size.width = right - left + 1;
+		_size.height = bottom - top + 1;
+		
+		return this;
+	}
+	
+	/**
+	 * Expands the bounds to encompass the given point.
+	 * 
+	 * @param p
+	 * 		The point to cover.
+	 */
+	public AxisAlignedBoxBounds combineWith(Point p)
+	{
+		if (p == null) return this;
+		
+		return combineWith(new AxisAlignedBoxBounds(p, new Dimension(1, 1)));
+	}
+
+	@Override
+	public boolean contains(int x, int y)
+	{
+		return (x >= _topLeft.x &&
+				x < (_topLeft.x + _size.width) &&
+				y >= _topLeft.y &&
+				y < (_topLeft.y + _size.height));
+	}
+
+	@Override
+	public int getMaxX()
+	{
+		return _topLeft.x + _size.width - 1;
+	}
+
+	@Override
+	public int getMinX()
+	{
+		return _topLeft.x;
+	}
+
+	@Override
+	public int getMaxY()
+	{
+		return _topLeft.y + _size.height - 1;
+	}
+
+	@Override
+	public int getMinY()
+	{
+		return _topLeft.y;
+	}
+	
+	@Override
+	public AxisAlignedBoxBounds translate(int x, int y)
+	{
+		_topLeft.add(x, y);
+		
+		return this;
+	}
+
+	@Override
+	public boolean equals(Bounds other)
+	{
+		if (other instanceof AxisAlignedBoxBounds) {
+			return this.getTopLeft().equals(((AxisAlignedBoxBounds) other).getTopLeft()) && this.getSize().equals(((AxisAlignedBoxBounds) other).getSize());
+		}
+		
+		return false;
+	}
+	
+	@Override
+	public boolean intersects(AxisAlignedBoxBounds other)
+	{
+		if (other == null) return false;
+		
+		boolean completelyAbove = other.getMaxY() < this.getMinY();
+		boolean completelyBelow = other.getMinY() > this.getMaxY();
+		boolean completelyLeft = other.getMaxX() < this.getMinX();
+		boolean completelyRight = other.getMinX() > this.getMaxX();
+		
+		return !(completelyAbove || completelyBelow || completelyLeft || completelyRight);
+	}
+
+	@Override
+	public boolean intersects(CombinationBounds other)
+	{
+		// Defer to the CombinationBounds routine
+		return other.intersects(this);
+	}
+
+	@Override
+	public boolean intersects(EllipticalBounds other)
+	{
+		// Defer to the EllipticalBounds routine
+		return other.intersects(this);
+	}
+
+	@Override
+	public boolean intersects(PolygonBounds other)
+	{
+		// Defer to the PolygonBounds routine
+		return other.intersects(this);
+	}
+	
+	/** Gets an array of four points representing the corners of the box. */
+	public Point[] getCorners()
+	{
+		Point[] corners = new Point[4];
+		
+		corners[0] = new Point(getMinX(), getMinY());
+		corners[1] = new Point(getMaxX(), getMinY());
+		corners[2] = new Point(getMaxX(), getMaxY());
+		corners[3] = new Point(getMinX(), getMaxY());
+		
+		return corners;
+	}
+	
+	/** Gets an array of four lines representing the border of this Bounds. */
+	public Line[] getBorderLines()
+	{
+		Line[] lines = new Line[4];
+		Point[] corners = getCorners();
+		
+		lines[0] = new Line(corners[0], corners[1]);
+		lines[1] = new Line(corners[1], corners[2]);
+		lines[2] = new Line(corners[2], corners[3]);
+		lines[3] = new Line(corners[3], corners[0]);
+		
+		return lines;
+	}
+
+	@Override
+	public double getArea()
+	{
+		return _size.width * _size.height;
+	}
+
+	@Override
+	public boolean perimeterContains(Point p, double error)
+	{
+		Line[] borderLines = getBorderLines();
+		
+		for (Line borderLine : borderLines) {
+			if (borderLine.contains(p, error)) return true;
+		}
+		
+		return false;
+	}
+
+	@Override
+	public boolean completelyContains(AxisAlignedBoxBounds other)
+	{
+		if (other == null) return false;
+		
+		return contains(other.getTopLeft()) && contains(other.getBottomRight());
+	}
+
+	@Override
+	public boolean completelyContains(CombinationBounds other)
+	{
+		if (other == null) return false;
+		
+		return completelyContains(getEnclosing(other));
+	}
+
+	@Override
+	public boolean completelyContains(EllipticalBounds other)
+	{
+		if (other == null) return false;
+		
+		return completelyContains(getEnclosing(other));
+	}
+
+	@Override
+	public boolean completelyContains(PolygonBounds other)
+	{
+		if (other == null) return false;
+		
+		return completelyContains(getEnclosing(other));
+	}
+	
+	/** Gets a polygon that represents the same area as this box. */
+	public PolygonBounds getPolygon()
+	{
+		return PolygonBounds.fromBox(this);
+	}
+	
+	/** Gets the area of intersection between this box and the other. */
+	public AxisAlignedBoxBounds intersectionWith(AxisAlignedBoxBounds other)
+	{
+		if (intersects(other)) {
+			int minX = getMinX() > other.getMinX() ? getMinX() : other.getMinX();
+			int minY = getMinY() > other.getMinY() ? getMinY() : other.getMinY();
+			int maxX = getMaxX() < other.getMaxX() ? getMaxX() : other.getMaxX();
+			int maxY = getMaxY() < other.getMaxY() ? getMaxY() : other.getMaxY();
+			
+			return new AxisAlignedBoxBounds(minX, minY, maxX - minX + 1, maxY - minY + 1);
+		} else {
+			return null;
+		}
+	}
+	
+	/** Gets the bounds that is linearly-interpolated between the two given bounds. */
+	public static AxisAlignedBoxBounds lerp(AxisAlignedBoxBounds a, AxisAlignedBoxBounds b, float t)
+	{
+		if (a == null || b == null) return null;
+		
+		int minX = (int) Util.lerp(a.getMinX(), b.getMinX(), t);
+		int minY = (int) Util.lerp(a.getMinY(), b.getMinY(), t);
+		int width = (int) Util.lerp(a.getWidth(), b.getWidth(), t);
+		int height = (int) Util.lerp(a.getHeight(), b.getHeight(), t);
+		
+		return new AxisAlignedBoxBounds(minX, minY, width, height);
+	}
+	
+	@Override
+	public AxisAlignedBoxBounds clone()
+	{
+		return new AxisAlignedBoxBounds(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/core/bounds/Bounds.java
===================================================================
--- /trunk/src/org/expeditee/core/bounds/Bounds.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/bounds/Bounds.java	(revision 1097)
@@ -0,0 +1,118 @@
+package org.expeditee.core.bounds;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.expeditee.core.Point;
+
+/**
+ * The Bounds interface represents objects that denote a subset of points in the
+ * 2-dimensional plane. All points in the plane are either inside or outside the bounds,
+ * and methods are given for determining which category individual or groups of points
+ * belong to. 
+ * 
+ * @author cts16
+ */
+public abstract class Bounds {
+
+	/**
+	 * Whether the given (x,y) coordinate is inside the borders of this bounds.
+	 */
+	public abstract boolean contains(int x, int y);
+	
+	/**
+	 * Whether the given point is inside the borders of this bounds.
+	 */
+	public boolean contains(Point p)
+	{
+		return contains(p.x, p.y);
+	}
+	
+	/**
+	 * Whether there exists a point that is within this bounds and the other bounds.
+	 */
+	public boolean intersects(Bounds other)
+	{
+		if (other == null) return false;
+		
+		if (other instanceof AxisAlignedBoxBounds) {
+			return intersects((AxisAlignedBoxBounds) other);
+		} else if (other instanceof CombinationBounds) {
+			return intersects((CombinationBounds) other);
+		} else if (other instanceof EllipticalBounds) {
+			return intersects((EllipticalBounds) other);
+		} else if (other instanceof PolygonBounds) {
+			return intersects((PolygonBounds) other);
+		}
+
+		return false;
+	}
+	public abstract boolean intersects(AxisAlignedBoxBounds other);
+	public abstract boolean intersects(CombinationBounds other);
+	public abstract boolean intersects(EllipticalBounds other);
+	public abstract boolean intersects(PolygonBounds other);
+	
+	/** Gets the x-coordinate of the right-most point in this bounds. */
+	public abstract int getMaxX();
+	/** Gets the x-coordinate of the left-most point in this bounds. */
+	public abstract int getMinX();
+	/** Gets the y-coordinate of the bottom-most point in this bounds. */
+	public abstract int getMaxY();
+	/** Gets the y-coordinate of the top-most point in this bounds. */
+	public abstract int getMinY();
+	
+	public Point getCentre()
+	{
+		return new Point((getMaxX() + getMinX()) / 2, (getMaxY() + getMinY()) / 2);
+	}
+	
+	/** Whether the set of points covered by this bounds is the same as the other bounds. */
+	public abstract boolean equals(Bounds other);
+	
+	/** Gets the total area covered by this bounds. */
+	public abstract double getArea();
+	
+	/** Whether the given point lies on the border of this bounds (within the given area). */
+	public abstract boolean perimeterContains(Point p, double error);
+	
+	/** Whether the other bounds is entirely inside this bounds. */
+	public boolean completelyContains(Bounds other)
+	{
+		if (other == null) return false;
+		
+		if (other instanceof AxisAlignedBoxBounds) {
+			return completelyContains((AxisAlignedBoxBounds) other);
+		} else if (other instanceof CombinationBounds) {
+			return completelyContains((CombinationBounds) other);
+		} else if (other instanceof EllipticalBounds) {
+			return completelyContains((EllipticalBounds) other);
+		} else if (other instanceof PolygonBounds) {
+			return completelyContains((PolygonBounds) other);
+		}
+
+		return false;
+	}
+	public abstract boolean completelyContains(AxisAlignedBoxBounds other);
+	public abstract boolean completelyContains(CombinationBounds other);
+	public abstract boolean completelyContains(EllipticalBounds other);
+	public abstract boolean completelyContains(PolygonBounds other);
+	
+	/** Translates the bounds by the given x/y amounts. */
+	public abstract Bounds translate(int x, int y);
+	
+	/** Gets all points contained by the bounds. */
+	public Set<Point> getAllContainedPoints()
+	{
+		Set<Point> ret = new HashSet<Point>();
+		for (int x = getMinX(); x <= getMaxX(); x++) {
+			for (int y = getMinY(); y <= getMaxY(); y++) {
+				if (contains(x, y)) ret.add(new Point(x, y));
+			}
+		}
+		
+		return ret;
+	}
+	
+	@Override
+	public abstract Bounds clone();
+}
Index: /trunk/src/org/expeditee/core/bounds/CombinationBounds.java
===================================================================
--- /trunk/src/org/expeditee/core/bounds/CombinationBounds.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/bounds/CombinationBounds.java	(revision 1097)
@@ -0,0 +1,259 @@
+package org.expeditee.core.bounds;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.expeditee.core.Point;
+
+/**
+ * The CombinationBounds class represents a collection of bounds, which
+ * may be disjoint. A point is considered inside the CombinationBounds if
+ * it is inside any of the individual bounds in the collection (i.e. if it
+ * is inside the conjunction of the individual bounds). If the combination
+ * bounds has no component bounds, then no points are inside it.
+ * 
+ * @author cts16
+ */
+public class CombinationBounds extends Bounds {
+
+	/** The collection of bounds. */
+	protected List<Bounds> _bounds;
+	
+	/** Default constructor. */
+	public CombinationBounds()
+	{
+		this(null);
+	}
+	
+	/** Standard constructor. */
+	public CombinationBounds(Bounds b)
+	{
+		_bounds = new LinkedList<Bounds>();
+		add(b);
+	}
+	
+	/** Copy constructor. */
+	public CombinationBounds(CombinationBounds other)
+	{
+		_bounds = new LinkedList<Bounds>();
+		for (Bounds b : other._bounds) add(b);
+	}
+	
+	/** Adds a bounds to the combination. */
+	public CombinationBounds add(Bounds b)
+	{
+		// Only valid Bounds are allowed in the collection
+		if (b == null) return this;
+		
+		_bounds.add(b);
+		
+		return this;
+	}
+	
+	/** Gets the bounds at the given index in this combination. */
+	public Bounds get(int index)
+	{
+		if (0 <= index && index < _bounds.size()) return _bounds.get(index);
+		
+		return null;
+	}
+	
+	/** Removes all component bounds. */
+	public CombinationBounds clear()
+	{
+		_bounds.clear();
+		
+		return this;
+	}
+	
+	/** Whether this combination bounds has no component bounds. */
+	public boolean isEmpty()
+	{
+		return _bounds.isEmpty();
+	}
+	
+	@Override
+	public boolean contains(int x, int y)
+	{
+		// If any component bounds contains the point, then the combination as
+		// a whole does as well
+		for (Bounds b : _bounds) {
+			if (b.contains(x, y)) return true;
+		}
+		
+		// No component contained the point
+		return false;
+	}
+
+	@Override
+	public int getMaxX() 
+	{
+		int maxX = Integer.MIN_VALUE;
+		
+		for (Bounds b : _bounds) {
+			int thisX = b.getMaxX();
+			if (thisX > maxX) maxX = thisX;
+		}
+		
+		return maxX;
+	}
+
+	@Override
+	public int getMinX()
+	{
+		int minX = Integer.MAX_VALUE;
+		
+		for (Bounds b : _bounds) {
+			int thisX = b.getMinX();
+			if (thisX < minX) minX = thisX;
+		}
+		
+		return minX;
+	}
+
+	@Override
+	public int getMaxY()
+	{
+		int maxY = Integer.MIN_VALUE;
+		
+		for (Bounds b : _bounds) {
+			int thisY = b.getMaxY();
+			if (thisY > maxY) maxY = thisY;
+		}
+		
+		return maxY;
+	}
+
+	@Override
+	public int getMinY()
+	{
+		int minY = Integer.MAX_VALUE;
+		
+		for (Bounds b : _bounds) {
+			int thisY = b.getMinY();
+			if (thisY < minY) minY = thisY;
+		}
+		
+		return minY;
+	}
+	
+	@Override
+	public CombinationBounds translate(int x, int y)
+	{
+		for (Bounds b : _bounds) {
+			b.translate(x, y);
+		}
+		
+		return this;
+	}
+
+	@Override
+	public boolean equals(Bounds other)
+	{
+		if (other instanceof CombinationBounds) {
+			CombinationBounds c = (CombinationBounds) other;
+			if (_bounds.size() == c._bounds.size()) {
+				for (int i = 0; i < _bounds.size(); i++) {
+					if (!(_bounds.get(i).equals(c._bounds.get(i)))) return false;
+				}
+				return true;
+			}
+		}
+		return false;
+	}
+	
+	@Override
+	public boolean intersects(Bounds other)
+	{
+		for (Bounds b : _bounds) {
+			if (b.intersects(other)) return true;
+		}
+		
+		return false;
+	}
+
+	@Override
+	public boolean intersects(AxisAlignedBoxBounds other)
+	{
+		return intersects((Bounds) other);
+	}
+
+	@Override
+	public boolean intersects(CombinationBounds other)
+	{
+		return intersects((Bounds) other);
+	}
+
+	@Override
+	public boolean intersects(EllipticalBounds other)
+	{
+		return intersects((Bounds) other);
+	}
+
+	@Override
+	public boolean intersects(PolygonBounds other)
+	{
+		return intersects((Bounds) other);
+	}
+
+	@Override
+	public double getArea()
+	{
+		// TODO Complete. Difficult as have to take into account overlapping areas. cts16
+		System.err.println("Unimplemented method: CombinationBounds.getArea()");
+		return 0;
+	}
+
+	@Override
+	public boolean perimeterContains(Point p, double error)
+	{
+		for (Bounds b : _bounds) {
+			if (b.perimeterContains(p, error)) {
+				for (Bounds b2 : _bounds) {
+					if (b2 != b && b2.contains(p) && !b2.perimeterContains(p, error)) return false;
+				}
+				return true;
+			}
+		}
+		
+		return false;
+	}
+
+	@Override
+	public boolean completelyContains(AxisAlignedBoxBounds other)
+	{
+		// TODO Complete. Difficult as we have to take into account conjoined areas. cts16
+		System.err.println("Unimplemented method: CombinationBounds.completelyContains(AxisAlignedBoxBounds)");
+		return false;
+	}
+
+	@Override
+	public boolean completelyContains(CombinationBounds other)
+	{
+		// TODO Complete. Difficult as we have to take into account conjoined areas. cts16
+		System.err.println("Unimplemented method: CombinationBounds.completelyContains(CombinationBounds)");
+		return false;
+	}
+
+	@Override
+	public boolean completelyContains(EllipticalBounds other)
+	{
+		// TODO Complete. Difficult as we have to take into account conjoined areas. cts16
+		System.err.println("Unimplemented method: CombinationBounds.completelyContains(EllipticalBounds)");
+		return false;
+	}
+
+	@Override
+	public boolean completelyContains(PolygonBounds other)
+	{
+		// TODO Complete. Difficult as we have to take into account conjoined areas. cts16
+		System.err.println("Unimplemented method: CombinationBounds.completelyContains(PolygonBounds)");
+		return false;
+	}
+	
+	@Override
+	public CombinationBounds clone()
+	{
+		return new CombinationBounds(this);
+	}
+}
Index: /trunk/src/org/expeditee/core/bounds/CombinationBoxBounds.java
===================================================================
--- /trunk/src/org/expeditee/core/bounds/CombinationBoxBounds.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/bounds/CombinationBoxBounds.java	(revision 1097)
@@ -0,0 +1,30 @@
+package org.expeditee.core.bounds;
+
+/**
+ * The CombinationBoxBounds represents a combination bounds where every
+ * component bounds is an axis-aligned box.
+ * 
+ * @author cts16
+ */
+public class CombinationBoxBounds extends CombinationBounds {
+
+	/** Default constructor. */
+	public CombinationBoxBounds()
+	{
+		super();
+	}
+	/** Standard constructor. */
+	public CombinationBoxBounds(AxisAlignedBoxBounds b)
+	{
+		super(b);
+	}
+	
+	@Override
+	public CombinationBoxBounds add(Bounds b)
+	{
+		// Ensure we are only adding boxes to the underlying bounds
+		if (b instanceof AxisAlignedBoxBounds) super.add(b);
+		
+		return this;
+	}
+}
Index: /trunk/src/org/expeditee/core/bounds/EllipticalBounds.java
===================================================================
--- /trunk/src/org/expeditee/core/bounds/EllipticalBounds.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/bounds/EllipticalBounds.java	(revision 1097)
@@ -0,0 +1,310 @@
+package org.expeditee.core.bounds;
+
+import org.expeditee.Util;
+import org.expeditee.core.Dimension;
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+
+/**
+ * A bounds in the shape of an ellipse.
+ * 
+ * @author cts16
+ */
+public class EllipticalBounds extends Bounds {
+
+	/** The location of the centre of the ellipse. */
+	private Point _centre;
+	/** The diameters of the ellipse in the x- and y-dimension. */
+	private Dimension _diameters;
+	
+	/** Standard constructor. */
+	public EllipticalBounds(Point centre, Dimension diameters)
+	{
+		setCentre(centre);
+		setDiameters(diameters);
+	}
+	
+	/** Constructor for a circular bounds. */
+	public EllipticalBounds(Point centre, int diameter)
+	{
+		this(centre, new Dimension(diameter, diameter));
+	}
+	
+	/** Copy constructor. */
+	public EllipticalBounds(EllipticalBounds other)
+	{
+		setCentre(other._centre);
+		setDiameters(other._diameters);
+	}
+	
+	/** Sets the location of the centre of this elliptical bounds. */
+	public void setCentre(Point centre)
+	{
+		// Control referential access to private variables
+		_centre = new Point(centre);
+	}
+	
+	/** Sets the size of the diameters of this elliptical bounds. */
+	public void setDiameters(Dimension diameters)
+	{
+		// Control referential access to private variables
+		_diameters = new Dimension(diameters);
+		
+		// Diameters must be positive
+		_diameters.absolve();
+	}
+	
+	/** Gets the centre of the elliptical bounds. */
+	public Point getCentre()
+	{
+		// Control referential access to private variables
+		return _centre.clone();
+	}
+	
+	/** Gets the size of the diameters of the elliptical bounds. */
+	public Dimension getDiameters()
+	{
+		// Control referential access to private variables
+		return _diameters.clone();
+	}
+	
+	@Override
+	public boolean contains(int x, int y)
+	{
+		// If the ellipse is degenerate then it can't contain anything
+		if (_diameters.getArea() == 0) return false;
+		
+		// Get the position of the given coordinates with respect to the ellipse centre 
+		Point pRel = new Point(x - _centre.x, y - _centre.y);
+		
+		// x^2 / r_x^2 + y^2 / r_y^2 must be <= 1
+		// (using diameters instead of radii hence the 4.0f value)
+		float xNorm = (((float) pRel.x) * pRel.x) / (((float) _diameters.width) * _diameters.width);
+		float yNorm = (((float) pRel.y) * pRel.y) / (((float) _diameters.height) * _diameters.height);
+		return (xNorm + yNorm <= 4.0f);
+	}
+	
+	@Override
+	public int getMaxX()
+	{
+		return _centre.x + (_diameters.width / 2 + _diameters.width % 2);
+	}
+
+	@Override
+	public int getMinX()
+	{
+		return _centre.x -(_diameters.width / 2 + _diameters.width % 2);
+	}
+
+	@Override
+	public int getMaxY()
+	{
+		return _centre.y + (_diameters.height / 2 + _diameters.height % 2);
+	}
+
+	@Override
+	public int getMinY()
+	{
+		return _centre.y - (_diameters.height / 2 + _diameters.height % 2);
+	}
+
+	@Override
+	public EllipticalBounds translate(int x, int y)
+	{
+		_centre.add(x, y);
+		
+		return this;
+	}
+	
+	@Override
+	public boolean equals(Bounds other)
+	{
+		if (other instanceof EllipticalBounds) {
+			EllipticalBounds e = (EllipticalBounds) other;
+			return e._centre.equals(this._centre) && e._diameters.equals(this._diameters);
+		}
+		return false;
+	}
+
+	@Override
+	public boolean intersects(AxisAlignedBoxBounds other)
+	{
+		// Treat the box as a polygon
+		return intersects(other.getPolygon());
+	}
+
+	@Override
+	public boolean intersects(CombinationBounds other)
+	{
+		// Defer to the CombinationBounds routine
+		return other.intersects(this);
+	}
+
+	@Override
+	public boolean intersects(EllipticalBounds other)
+	{
+		// TODO: Implement. cts16
+		System.err.println("Unimplemented method: EllipticalBounds.intersects(EllipticalBounds)");
+		return false;
+	}
+
+	@Override
+	public boolean intersects(PolygonBounds other)
+	{
+		// If any point in the polygon is inside the ellipse then they intersect
+		for (Point p : other.getVertices()) {
+			if (contains(p)) return true;
+		}
+		
+		// If no polygon point is contained, may still intersect polygon boundary lines
+		for (Line line : other.getBorderLines()) {
+			if (isIntersectedByLine(line)) return true;
+		}
+		
+		return false;
+	}
+
+	@Override
+	public double getArea()
+	{
+		// Area = pi * r_x * r_y
+		// (dividing by 4.0 because using diameters instead of radii)
+		return Math.PI * _diameters.getArea() / 4.0;
+	}
+
+	@Override
+	public boolean perimeterContains(Point p, double error)
+	{
+		// If the given point is null, we can't test
+		if (p == null) return false;
+		
+		// Find the distance from the centre of the ellipse to the given point
+		double pRadialDistance = Point.distanceBetween(p, _centre);
+		
+		// Find the distance to the ellipse border in the same direction
+		double borderRadialDistance = radialDistanceAtAngle(getAngleToPoint(p));
+		
+		// If the difference between these two distances is within error then we are on the perimeter
+		return Math.abs(borderRadialDistance - pRadialDistance) <= error;
+		
+	}
+
+	@Override
+	public boolean completelyContains(AxisAlignedBoxBounds other)
+	{
+		// Can't contain nothing
+		if (other == null) return false;
+		
+		// It is sufficient to test if all four corners are contained
+		Point[] corners = other.getCorners();
+		for (Point corner : corners) {
+			if (!contains(corner)) return false;
+		}
+		
+		return true;
+	}
+
+	@Override
+	public boolean completelyContains(CombinationBounds other)
+	{
+		// Can't contain nothing
+		if (other == null) return false;
+		
+		// Must contain all sub-bounds
+		for (Bounds bound : other._bounds) {
+			if (!completelyContains(bound)) return false;
+		}
+		
+		return true;
+	}
+
+	@Override
+	public boolean completelyContains(EllipticalBounds other)
+	{
+		// TODO: Implement. cts16
+		System.err.println("Unimplemented method: EllipticalBounds.completelyContains(EllipticalBounds)");
+		return false;
+	}
+
+	@Override
+	public boolean completelyContains(PolygonBounds other)
+	{
+		// Can't contain nothing
+		if (other == null) return false;
+		
+		// It is sufficient to test if all vertices are contained
+		Point[] vertices = other.getVertices();
+		for (Point vertex : vertices) {
+			if (!contains(vertex)) return false;
+		}
+		
+		return true;
+	}
+	
+	/** Gets the distance to the border of the ellipse at a given angle. */
+	public double radialDistanceAtAngle(double angle)
+	{
+		double xComponent = (_diameters.width / 2.0) * Math.cos(angle);
+		double yComponent = (_diameters.height / 2.0) * Math.sin(angle);
+		return Math.sqrt(xComponent * xComponent + yComponent * yComponent);
+	}
+	
+	/** Gets the bearing angle from the centre of the ellipse to a given point. */
+	public double getAngleToPoint(Point p)
+	{
+		if (p == null) return Double.NaN;
+		
+		Point pRel = Point.difference(p, _centre);
+		return Math.atan2(pRel.y, pRel.x);
+	}
+	
+	/** Whether the given line intersects the perimeter of this ellipse. */
+	public boolean isIntersectedByLine(Line line)
+	{
+		// Can't intersect with nothing
+		if (line == null) return false;
+		
+		// Get the t-values at the points of intersections (if there are any)
+		double r_x = _diameters.width / 2.0;
+		double r_y = _diameters.height / 2.0;
+		Point lineStartRel = Point.difference(line.getFirstEnd(), _centre);
+		double x_0 = lineStartRel.x;
+		double y_0 = lineStartRel.y;
+		double x_d = line.deltaX();
+		double y_d = line.deltaY();
+		double r_x_squared = r_x * r_x;
+		double r_y_squared = r_y * r_y;
+		double x_0_squared = x_0 * x_0;
+		double y_0_squared = y_0 * y_0;
+		double x_d_squared = x_d * x_d;
+		double y_d_squared = y_d * y_d;
+		double[] roots = Util.quadraticEquation(
+													x_d_squared * r_y_squared + y_d_squared * r_x_squared,
+													2 * (r_y_squared * x_0 * x_d + r_x_squared * y_0 * y_d),
+													r_y_squared * x_0_squared + r_x_squared * y_0_squared - r_x_squared * r_y_squared
+		);
+		
+		// No roots, no intersection
+		if (roots == null) return false;
+		
+		// See if any of the roots is inside the line segment
+		for (double root : roots) {
+			if (0.0 <= root && root <= 1.0) return true;
+		}
+		
+		return false;
+	}
+	
+	/** Creates a polygonal approximation of this ellipse. */
+	public PolygonBounds getPolygon(int nSegments)
+	{
+		return PolygonBounds.fromEllipse(this, nSegments);
+	}
+	
+	@Override
+	public EllipticalBounds clone()
+	{
+		return new EllipticalBounds(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/core/bounds/PolygonBounds.java
===================================================================
--- /trunk/src/org/expeditee/core/bounds/PolygonBounds.java	(revision 1097)
+++ /trunk/src/org/expeditee/core/bounds/PolygonBounds.java	(revision 1097)
@@ -0,0 +1,541 @@
+package org.expeditee.core.bounds;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+
+/**
+ * A bounds that is defined as the area enclosed by a polygon formed
+ * from a set of points. When considering if something is enclosed by
+ * a polygon, the set of points is always considered closed (even if
+ * the first and last points arenn't equal).
+ * 
+ * @author cts16
+ */
+public class PolygonBounds extends Bounds {
+
+	/** The points that form this polygon. */
+	private List<Point> _points;
+	
+	/** The right-most extent of this bounds. */
+	Integer _maxX;
+	/** The left-most extent of this bounds. */
+	Integer _minX;
+	/** The down-most extent of this bounds. */
+	Integer _maxY;
+	/** The up-most extent of this bounds. */
+	Integer _minY;
+	
+	/** Standard constructor (creates a degenerate polygon with no points). */
+	public PolygonBounds()
+	{
+		_points = new LinkedList<Point>();
+		_maxX = _minX = _maxY = _minY = null;
+	}
+	
+	/** Copy constructor. */
+	public PolygonBounds(PolygonBounds other)
+	{
+		this();
+		for (Point p : other._points) {
+			addPoint(p);
+		}
+	}
+	
+	/**
+	 * Constructs a polygon from a set of lines. Lines should be connected
+	 * end to end, otherwise the resulting polygon may not match the given input.
+	 */
+	public PolygonBounds(Line[] lines)
+	{
+		this();
+		for (Line line : lines) {
+			addPoint(line.getFirstEnd());
+		}
+		addPoint(lines[lines.length - 1].getSecondEnd());
+	}
+	
+	/** Adds a new point to the polygon. */
+	public void addPoint(Point p)
+	{
+		addPoint(p.x, p.y);
+	}
+	
+	/** Adds a new point to the polygon. */
+	public void addPoint(int x, int y)
+	{
+		_points.add(new Point(x, y));
+		if (_maxX == null || x > _maxX.intValue()) _maxX = new Integer(x);
+		if (_minX == null || x < _minX.intValue()) _minX = new Integer(x);
+		if (_maxY == null || y > _maxY.intValue()) _maxY = new Integer(y);
+		if (_minY == null || y < _minY.intValue()) _minY = new Integer(y);
+	}
+	
+	/** Gets the point at the given index in this polygon. */
+	public Point getPoint(int index)
+	{
+		if (0 <= index && index < getPointCount()) return _points.get(index).clone();
+		
+		return null;
+	}
+	
+	/** Returns the number of points in this polygon. */
+	public int getPointCount()
+	{
+		return _points.size();
+	}
+	
+	/** Returns a copy of the points of this polygon as an array. */
+	public Point[] toArray()
+	{
+		Point[] ret = new Point[_points.size()];
+		
+		for (int i = 0; i < _points.size(); i++) {
+			ret[i] = _points.get(i).clone();
+		}
+		
+		return ret;
+	}
+	
+	@Override
+	public PolygonBounds translate(int x, int y)
+	{
+		for (Point p : _points) p.add(x, y);
+		
+		if (_maxX != null) _maxX = new Integer(_maxX.intValue() + x);
+		if (_minX != null) _minX = new Integer(_minX.intValue() + x);
+		if (_maxY != null) _maxY = new Integer(_maxY.intValue() + y);
+		if (_minY != null) _minY = new Integer(_minY.intValue() + y);
+		
+		return this;
+	}
+	
+	/**
+	 * Rotates all of the points in this polygon about the given x/y coordinate.
+	 * 
+	 * @param angle Clockwise angle of rotation in radians.
+	 */ 
+	public PolygonBounds rotate(double angle, int x, int y)
+	{
+		// Rotate each point
+		for (Point p : _points) {
+			p.rotate(angle, x, y);
+		}
+		
+		// Non-trivial change to mins/maxs, so recalculate
+		recalculateExtents();
+		
+		return this;
+	}
+	
+	/**
+	 * Rotates the polygon around a point.
+	 * 
+	 * @param angle
+	 * 		The clockwise angle to rotate the polygon through (in radians).
+	 * 
+	 * @param cor
+	 * 		The centre of rotation.
+	 */
+	public void rotate(double angle, Point cor)
+	{
+		rotate(angle, cor.x, cor.y);
+	}
+	
+	/**
+	 * Code adapted from java.awt.Polygon.contains(double x, double y).
+	 * 
+	 * @author cts16
+	 */
+	@Override
+	public boolean contains(int x, int y) {
+		int npoints = _points.size();
+		if (npoints <= 2) {
+            return false;
+        }
+		
+        int hits = 0;
+
+        int lastx = _points.get(npoints - 1).x;
+        int lasty = _points.get(npoints - 1).y;
+        int curx, cury;
+
+        // Walk the edges of the polygon
+        for (int i = 0; i < npoints; lastx = curx, lasty = cury, i++) {
+            curx = _points.get(i).x;
+            cury = _points.get(i).y;
+
+            if (cury == lasty) {
+                continue;
+            }
+
+            int leftx;
+            if (curx < lastx) {
+                if (x >= lastx) {
+                    continue;
+                }
+                leftx = curx;
+            } else {
+                if (x >= curx) {
+                    continue;
+                }
+                leftx = lastx;
+            }
+
+            double test1, test2;
+            if (cury < lasty) {
+                if (y < cury || y >= lasty) {
+                    continue;
+                }
+                if (x < leftx) {
+                    hits++;
+                    continue;
+                }
+                test1 = x - curx;
+                test2 = y - cury;
+            } else {
+                if (y < lasty || y >= cury) {
+                    continue;
+                }
+                if (x < leftx) {
+                    hits++;
+                    continue;
+                }
+                test1 = x - lastx;
+                test2 = y - lasty;
+            }
+
+            if (test1 < (test2 / (lasty - cury) * (lastx - curx))) {
+                hits++;
+            }
+        }
+
+        return ((hits & 1) != 0);
+	}
+	
+	@Override
+	public int getMaxX() {
+		return _maxX != null ? _maxX : 0;
+	}
+
+	@Override
+	public int getMinX() {
+		return _minX != null ? _minX : 0;
+	}
+
+	@Override
+	public int getMaxY() {
+		return _maxY != null ? _maxY : 0;
+	}
+
+	@Override
+	public int getMinY() {
+		return _minY != null ? _minY : 0;
+	}
+
+	@Override
+	public boolean equals(Bounds other) {
+		if (other instanceof PolygonBounds) {
+			PolygonBounds p = (PolygonBounds) other;
+			if (p._points.size() == this._points.size()) {
+				for (int i = 0; i < p._points.size(); i++) {
+					if (!(p._points.get(i).equals(this._points.get(i)))) return false;
+				}
+				return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	/** Convenience method to create a polygon in the shape of a diamond. */
+	public static PolygonBounds getDiamond(int width, int height)
+	{
+		PolygonBounds diamond = new PolygonBounds();
+		
+		diamond.addPoint(0, -height / 2);
+		diamond.addPoint(width / 2, 0);
+		diamond.addPoint(0, height / 2);
+		diamond.addPoint(-width / 2, 0);
+		
+		return diamond;
+	}
+	
+	/** Convenience method to create a polygon in the shape of a triangle. */
+	public static PolygonBounds getTriangle(int base, int height)
+	{
+		PolygonBounds triangle = new PolygonBounds();
+		
+		triangle.addPoint(0, -height / 2);
+		triangle.addPoint(base / 2, height / 2);
+		triangle.addPoint(-base / 2, height / 2);
+		
+		return triangle;
+	}
+
+	@Override
+	public boolean intersects(AxisAlignedBoxBounds other) {
+		// Check if any polygon points are inside the box
+		for (Point p : _points) {
+			if (other.contains(p)) return true;
+		}
+		
+		// Check if any of the border lines intersect
+		Line[] boxLines = other.getBorderLines();
+		Line[] polyLines = getBorderLines();
+		if (Line.anyIntersections(boxLines, polyLines, Line.PIXEL_ERROR)) return true;
+		
+		// Check if any of the box points are inside the polygon
+		// (only test p1 as p2 is the p1 of another line)
+		for (Line boxLine : boxLines) {
+			if (contains(boxLine.getFirstEnd())) return true;
+		}
+		
+		// If all tests fail, no intersection
+		return false;
+	}
+
+	@Override
+	public boolean intersects(CombinationBounds other) {
+		// Defer to the CombinationBounds routine
+		return other.intersects(this);
+	}
+
+	@Override
+	public boolean intersects(EllipticalBounds other) {
+		// Defer to the EllipticalBounds routine
+		return other.intersects(this);
+	}
+
+	@Override
+	public boolean intersects(PolygonBounds other) {
+		// Check if any of the border lines intersect
+		Line[] otherLines = other.getBorderLines();
+		Line[] thisLines = getBorderLines();
+		if (Line.anyIntersections(otherLines, thisLines, Line.PIXEL_ERROR)) return true;
+		
+		// If no line intersections, one polygon must be inside the other to intersect
+		return other.contains(thisLines[0].getFirstEnd()) || this.contains(otherLines[0].getFirstEnd());
+	}
+	
+	/** Gets an array of the vertices that make up this polygon. */
+	public Point[] getVertices()
+	{
+		Point[] ret = new Point[_points.size()];
+		
+		for (int i = 0; i < _points.size(); i++) {
+			ret[i] = _points.get(i).clone();
+		}
+		
+		return ret;
+	}
+	
+	/** Gets an array of lines that represent the (closed) border of this polygon. */
+	public Line[] getBorderLines()
+	{
+		return getBorderLines(true);
+	}
+	
+	/**
+	 * Gets an array of lines that represent the border of this polygon.
+	 * 
+	 * @param autoClose
+	 * 		Whether to automatically create a line from the last point to the 
+	 * 		first to close this polygon (ignored if first and last point are equal).
+	 * 
+	 * @return
+	 * 		The array of border lines.
+	 */
+	public Line[] getBorderLines(boolean autoClose)
+	{
+		// Don't need to autoClose if first and last points are equal
+		autoClose = autoClose && _points.get(0).equals(_points.get(_points.size() - 1));
+		
+		// Calculate how many lines we will need to generate
+		int nLines = _points.size();
+		if (!autoClose) nLines--;
+		
+		// Generate the lines
+		Line[] lines = new Line[nLines];
+		for (int i = 0; i < nLines; i++) {
+			Point p1 = new Point(_points.get(i));
+			Point p2 = new Point(_points.get((i + 1) % _points.size()));
+			lines[i] = new Line(p1, p2);
+		}
+		
+		return lines;
+	}
+	
+	@Override
+	public double getArea()
+	{
+		if (_points.size() < 3) return 0.0;
+		
+		double area = 0.0;
+		
+		for (int i = 0; i < _points.size(); i++) {
+			Point p1 = _points.get(i);
+			Point p2 = _points.get((i + 1) % _points.size());
+			area += p1.x * p2.y - p2.x * p1.y;
+		}
+		
+		return Math.abs(area / 2);
+	}
+
+	@Override
+	public boolean perimeterContains(Point p, double error) {
+		Line[] lines = getBorderLines();
+		
+		for (Line line : lines) {
+			if (line.contains(p, error)) return true;
+		}
+		
+		return false;
+	}
+
+	@Override
+	public boolean completelyContains(AxisAlignedBoxBounds other)
+	{
+		// Treat the box as a polygon
+		return completelyContains(other.getPolygon());
+	}
+
+	@Override
+	public boolean completelyContains(CombinationBounds other)
+	{
+		// Can't contain nothing
+		if (other == null) return false;
+		
+		// Must contain all sub-bounds of the combination bounds
+		for (Bounds bounds : other._bounds) {
+			if (!completelyContains(bounds)) return false;
+		}
+		
+		return true;
+	}
+
+	@Override
+	public boolean completelyContains(EllipticalBounds other)
+	{
+		// Can't contain nothing
+		if (other == null) return false;
+		
+		// No polygon vertices can be inside the ellipse
+		for (Point vertex : _points) {
+			if (other.contains(vertex)) return false;
+		}
+		
+		// No boundary lines can intersect the ellipse
+		for (Line line : getBorderLines()) {
+			if (other.isIntersectedByLine(line)) return false;
+		}
+		
+		return true;
+	}
+
+	@Override
+	public boolean completelyContains(PolygonBounds other)
+	{
+		// Can't contain nothing
+		if (other == null) return false;
+		
+		// Must contain all points of other
+		for (Point p : other._points) {
+			if (!contains(p)) return false;
+		}
+		
+		// Can't be any line intersections
+		Line[] thisLines = getBorderLines();
+		Line[] otherLines = other.getBorderLines();
+		if (Line.anyIntersections(thisLines, otherLines, Line.PIXEL_ERROR)) return false;
+		
+		return true;
+	}
+	
+	/** Whether the given point is a vertex of this polygon. */
+	public boolean isVertex(Point p)
+	{
+		return _points.contains(p);
+	}
+	
+	/** Whether or not this polygon is closed (first and last points are equal). */
+	public boolean isClosed()
+	{
+		return _points.get(0).equals(_points.get(getPointCount() - 1));
+	}
+	
+	/** Closes this polygon (adds the first point as the last). */
+	public PolygonBounds close()
+	{
+		if (!isClosed()) addPoint(_points.get(0).clone());
+		
+		return this;
+	}
+	
+	/** Creates a polygon that represents the same area as a box bounds. */
+	public static PolygonBounds fromBox(AxisAlignedBoxBounds box)
+	{
+		if (box == null) return null;
+		
+		// Get the corners of the box
+		Point[] corners = box.getCorners();
+		
+		// Add the corners to a new polygon
+		PolygonBounds ret = new PolygonBounds();
+		for (Point corner : corners) ret.addPoint(corner);
+		
+		return ret.close();
+	}
+	
+	/**
+	 * Creates a polygon that approximates the given ellipse.
+	 * 
+	 * @param nSegments
+	 * 		The number of segments the resulting polygon should have. Must be at least 3.
+	 */ 
+	public static PolygonBounds fromEllipse(EllipticalBounds ellipse, int nSegments)
+	{
+		if (ellipse == null || nSegments < 3) return null;
+		
+		int centreX = ellipse.getCentre().x;
+		int centreY = ellipse.getCentre().y;
+		double xRadius = ellipse.getDiameters().width / 2.0;
+		double yRadius = ellipse.getDiameters().height / 2.0;
+		double radians = 0.0;
+		
+		PolygonBounds ret = new PolygonBounds();
+		for (int i = 0; i < nSegments; i++) {
+			int x = (int) (xRadius * Math.cos(radians)) + centreX;
+			int y = (int) (yRadius * Math.sin(radians)) + centreY;
+			ret.addPoint(x, y);
+			radians += (2.0 * Math.PI) / nSegments;
+		}
+		
+		return ret.close();
+	}
+	
+	@Override
+	public PolygonBounds clone()
+	{
+		return new PolygonBounds(this);
+	}
+	
+	/**
+	 * Recalculates the min/max x/y coordinates. Normally this should be done incrementally
+	 * as points are added, but sometimes you just need to completely redo them.
+	 */
+	private void recalculateExtents()
+	{
+		_maxX = _minX = _maxY = _minY = null;
+		
+		for (Point p : _points) {
+			if (_maxX == null || p.x > _maxX.intValue()) _maxX = new Integer(p.x);
+			if (_minX == null || p.x < _minX.intValue()) _minX = new Integer(p.x);
+			if (_maxY == null || p.y > _maxY.intValue()) _maxY = new Integer(p.y);
+			if (_minY == null || p.y < _minY.intValue()) _minY = new Integer(p.y);
+		}
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/ClipboardManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/ClipboardManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/ClipboardManager.java	(revision 1097)
@@ -0,0 +1,72 @@
+package org.expeditee.gio;
+
+import org.expeditee.core.Image;
+
+/**
+ * In charge of getting/setting the OS clipboard. Any Object can be
+ * set on the clipboard, but typically only other instances of Expeditee
+ * will be able to read this. Similarly generic data will only be available
+ * to get if it has been set by another Expeditee instance. Additionally,
+ * the clipboard data can have an image- or string-representation, which
+ * external applications that can copy/paste image/string data will be able
+ * to set/get.
+ * 
+ * @author cts16
+ */
+public abstract class ClipboardManager {
+
+	/**
+	 * Sets the clipboard to the given data. If data is null, should leave
+	 * the clipboard unchanged (however, if data is not null but all of its
+	 * fields are null, this will clear the clipboard).
+	 */
+	public abstract void set(ClipboardData data);
+	
+	/** Gets the data that is currently on the clipboard. */
+	public abstract ClipboardData get();
+	
+	/** Representation of the data on the clipboard. */
+	public static class ClipboardData {
+		
+		/** Any generic data on the clipboard (for Expeditee use only). */
+		public Object data;
+		/**
+		 * An image representation of the clipboard data
+		 * (for use when copy/pasting to/from image applications).
+		 */
+		public Image imageRepresentation;
+		/**
+		 * A string representation of the clipboard data
+		 * (for use when copy/pasting to/from text applications).
+		 */
+		public String stringRepresentation;
+		
+		/** Default constructor. */
+		public ClipboardData()
+		{
+			this(null, null, null);
+		}
+		
+		/** Standard constructor. */
+		public ClipboardData(Object data, Image imageRepresentation, String stringRepresentation)
+		{
+			this.data = data;
+			this.imageRepresentation = imageRepresentation;
+			this.stringRepresentation = stringRepresentation;
+		}
+		
+		/** Instantiator for purely-image data. */
+		public static ClipboardData fromImage(Image image)
+		{
+			return new ClipboardData(null, image, null);
+		}
+		
+		/** Instantiator for purely-string data. */
+		public static ClipboardData fromString(String string)
+		{
+			return new ClipboardData(null, null, string);
+		}
+		
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/DragAndDropManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/DragAndDropManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/DragAndDropManager.java	(revision 1097)
@@ -0,0 +1,203 @@
+package org.expeditee.gio;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.expeditee.core.Point;
+import org.expeditee.gui.DisplayController;
+import org.expeditee.importer.FileImporter;
+import org.expeditee.importer.FilePathImporter;
+import org.expeditee.importer.ImageImporter;
+import org.expeditee.importer.TextImporter;
+import org.expeditee.importer.pdfImporter;
+import org.expeditee.items.Item;
+import org.expeditee.items.Text;
+
+/** TODO: Comment. cts16 */
+public abstract class DragAndDropManager {
+	
+	/** The list of custom file importers registered with the DnD manager. */
+	private List<FileImporter> _customFileImporters = new LinkedList<FileImporter>();
+
+	/** The list of standard file importers registered with the DnD manager. */
+	private List<FileImporter> _standardFileImporters = new LinkedList<FileImporter>();
+	
+	/** Standard constructor. */
+	protected DragAndDropManager()
+	{
+		// Set up the standard file importers
+		_standardFileImporters.add(new ImageImporter());
+		_standardFileImporters.add(new pdfImporter());
+		_standardFileImporters.add(new TextImporter());
+		_standardFileImporters.add(new FilePathImporter()); // Filepath importer as last resort
+	}
+
+	/**
+	 * Adds a custom importer. If an importer competes with another importer for
+	 * handling the same file types, the importer that was added first will have
+	 * first serve.
+	 * 
+	 * @param importer
+	 *            The importer to add. Must not be null
+	 * 
+	 * @throws NullPointerException
+	 *             if importer is null.
+	 */
+	public void addCustomFileImporter(FileImporter importer)
+	{
+		if (importer == null) throw new NullPointerException("importer");
+		
+		if (!_customFileImporters.contains(importer)) {
+			_customFileImporters.add(importer);
+		}
+	}
+
+	/**
+	 * Removes a custom importer.
+	 * 
+	 * @param importer
+	 *            The importer to remove.
+	 * 
+	 * @throws NullPointerException
+	 *             if importer is null.
+	 */
+	public void removeCustomFileImporter(FileImporter importer)
+	{
+		if (importer == null) throw new NullPointerException("importer");
+		
+		_customFileImporters.remove(importer);
+	}
+
+	/**
+	 * Imports a string into expeditee's current frame
+	 * 
+	 * @param text
+	 *            The text content.
+	 * 
+	 * @param expediteeDropPoint
+	 *            The location in the current expeditee frame of where to drop
+	 *            the text item.
+	 */
+	public static Text importString(String text, Point expediteeDropPoint)
+	{
+		assert (DisplayController.getCurrentFrame() != null);
+		assert (text != null && text.length() > 0);
+
+		Text importedTextItem = new Text(DisplayController.getCurrentFrame().getNextItemID(), text);
+		importedTextItem.setPosition(expediteeDropPoint);
+
+		DisplayController.getCurrentFrame().addItem(importedTextItem);
+		DisplayController.requestRefresh(true);
+
+		return importedTextItem;
+	}
+
+	/** Imports a list of files into Expeditee's current frame. */
+	public void importFileList(List<? extends File> files, Point expediteeDropPoint) throws IOException
+	{
+		Point currentPoint = new Point(expediteeDropPoint);
+
+		 // import files one by one
+		for (File fileToImport : files) {
+
+			Item lastItem = importFile(fileToImport, currentPoint);
+
+			if (lastItem == null) {
+				currentPoint.y += 30;
+			} else {
+				currentPoint.y += lastItem.getBoundsHeight();
+			}
+			// of the item that was created
+
+			// TODO: Better placement strategy
+			// if (currentPoint.y > (Browser._theBrowser.getHeight() - 20))
+			// currentPoint.y = Browser._theBrowser.getHeight() - 20;
+		}
+	}
+
+	/**
+	 * Imports a file into Expeditee. Any custom importers registered get first attempt,
+	 * followed by the standard importers.
+	 * 
+	 * @param f
+	 *            The file to import.
+	 * 
+	 * @param expediteeDropPoint
+	 * 
+	 * @throws IOException
+	 */
+	public Item importFile(File f, Point expediteeDropPoint) throws IOException
+	{
+		assert (f != null);
+
+		// Check for custom importers first. They get preference to standard
+		// importing routines...
+		Item lastCreatedItem;
+		if (null == (lastCreatedItem = performFileImport(_customFileImporters, f, expediteeDropPoint))) {
+			// Standard file importing
+			lastCreatedItem = performFileImport(_standardFileImporters, f, expediteeDropPoint);
+		}
+		
+		return lastCreatedItem;
+	}
+
+	/**
+	 * Imports a single file into Expeditee's current frame. Importers are given the opportunity to
+	 * interpret the file in order of their addition to the list.
+	 */
+	private Item performFileImport(List<FileImporter> importers, File f, Point expediteeDropPoint) throws IOException
+	{
+		for (FileImporter fi : importers) {
+			Item lastCreated = fi.importFile(f, expediteeDropPoint);
+			
+			if (lastCreated != null) return lastCreated;
+		}
+
+		return null;
+	}
+	
+	/**
+	 * Converts a string formatted as a list of URIs into a list of Files.
+	 * 
+	 * Code adopted from SUN - java BUG ID 4899516 workaround for KDE/GNOME
+	 * Desktops
+	 * 
+	 * @param uriListString
+	 *            Formatted according to RFC 2483
+	 * 
+	 * @return The list of FILES in the uriListString. Never null.
+	 */
+	protected List<File> textURIListToFileList(String uriListString)
+	{
+		List<File> fileList = new LinkedList<File>();
+
+		for (StringTokenizer st = new StringTokenizer(uriListString, "\r\n"); st.hasMoreTokens();) {
+
+			String s = st.nextToken();
+
+			if (s.startsWith("#")) {
+				// the line is a comment (as per the RFC 2483)
+				continue;
+			}
+
+			try {
+
+				URI uri = new URI(s);
+				File file = new File(uri);
+				fileList.add(file);
+
+			} catch (URISyntaxException e) {
+				// malformed URI
+			} catch (IllegalArgumentException e) {
+				// the URI is not a valid 'file:' URI
+			}
+		}
+
+		return fileList;
+	}
+}
Index: /trunk/src/org/expeditee/gio/EcosystemManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/EcosystemManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/EcosystemManager.java	(revision 1097)
@@ -0,0 +1,211 @@
+package org.expeditee.gio;
+
+import org.expeditee.core.Image;
+import org.expeditee.core.TextLayout;
+import org.expeditee.gio.gesture.StandardGestureActions;
+import org.expeditee.gio.gesture.ExpediteeKBMGestureTranslator;
+import org.expeditee.gio.input.StandardInputEventListeners;
+import org.expeditee.gio.javafx.JavaFXEcosystemManager;
+import org.expeditee.gio.swing.SwingEcosystemManager;
+import org.expeditee.items.widgets.Widget;
+
+/**
+ * Central access point for all Graphics/Input/Output managers.
+ * 
+ * @author cts16
+ */
+public abstract class EcosystemManager {
+
+	/** Enumeration of the available types of ecosystem. */
+	public static enum Ecosystem {
+		Swing,
+		JavaFX
+	}
+	
+	/** The type of GIO ecosystem running in this session. */
+	private static Ecosystem _type;
+	
+	/** The clipboard manager in use for this session. */
+	private static ClipboardManager _clipboardManager;
+	/** The image manager in use for this session. */
+	private static ImageManager _imageManager;
+	/** The graphics manager in use for this session. */
+	private static GraphicsManager _graphicsManager;
+	/** The font manager in use for this session. */
+	private static FontManager _fontManager;
+	/** The text-layout manager in use for this session. */
+	private static TextLayoutManager _textLayoutManager;
+	/** The input manager in use for this session. */
+	private static InputManager _inputManager;
+	/** The misc. manager in use for this session. */
+	private static MiscManager _miscManager;
+	/** The drag-and-drop manager in use for this session. */
+	private static DragAndDropManager _dndManager;
+	
+	/** Shouldn't be instantiated, static class only. */
+	protected EcosystemManager()
+	{
+	}
+	
+	/** Creates an ecosystem for the given ecosystem-type. */
+	public static void createEcosystem(Ecosystem type)
+	{
+		// Remember the ecosystem type for future reference
+		_type = type;
+		
+		// Create the ecosystem for the given type
+		if (type == Ecosystem.Swing) {
+			SwingEcosystemManager.createSwingEcosystem();
+		} else if (type == Ecosystem.JavaFX) {
+			JavaFXEcosystemManager.createJavaFXEcosystem();
+		}
+	}
+	
+	/** Gets the type of GIO ecosystem running for this session. */
+	public static Ecosystem getType()
+	{
+		return _type;
+	}
+	
+	/** Adds an ecosystem-native interactive widget to the ecosystem. */
+	public static void addInteractiveWidget(Widget iw)
+	{
+		// Can't add nothing
+		if (iw == null) return;
+		
+		// Only add the widget if it is designed for the current ecosystem
+		if (iw.isSupportedOnCurrentEcosystem()) return;
+		
+		// Add the widget to the graphics and input managers
+		_graphicsManager.addInteractiveWidget(iw);
+		_inputManager.addInteractiveWidget(iw);
+	}
+	
+	/** Removes an ecosystem-native interactive widget from the ecosystem. */
+	public static void removeInteractiveWidget(Widget iw)
+	{
+		// Can't remove nothing
+		if (iw == null) return;
+		
+		// Add the widget to the graphics and input managers
+		_graphicsManager.removeInteractiveWidget(iw);
+		_inputManager.removeInteractiveWidget(iw);
+	}
+	
+	/** Sets the clipboard manager for this session. Can only be done once. */
+	public static void setClipboardManager(ClipboardManager clipboardManager)
+	{
+		if (_clipboardManager == null) {
+			_clipboardManager = clipboardManager;
+		}
+	}
+	
+	/** Gets the clipboard manager set for this session. */
+	public static ClipboardManager getClipboardManager()
+	{
+		return _clipboardManager;
+	}
+	
+	/** Sets the image manager for this session. Can only be done once. */
+	public static void setImageManager(ImageManager imageManager)
+	{
+		if (_imageManager == null) {
+			_imageManager = imageManager;
+			Image.setManager(imageManager);
+		}
+	}
+
+	/** Gets the image manager set for this session. */
+	public static ImageManager getImageManager()
+	{
+		return _imageManager;
+	}
+
+	/** Sets the graphics manager for this session. Can only be done once. */
+	public static void setGraphicsManager(GraphicsManager graphicsManager)
+	{
+		if (_graphicsManager == null) {
+			_graphicsManager = graphicsManager;
+		}
+	}
+
+	/** Gets the graphics manager set for this session. */
+	public static GraphicsManager getGraphicsManager()
+	{
+		return _graphicsManager;
+	}
+
+	/** Sets the font manager for this session. Can only be done once. */
+	public static void setFontManager(FontManager fontManager)
+	{
+		if (_fontManager == null) {
+			_fontManager = fontManager;
+		}
+	}
+
+	/** Gets the font manager set for this session. */
+	public static FontManager getFontManager()
+	{
+		return _fontManager;
+	}
+
+	/** Sets the text-layout manager for this session. Can only be done once. */
+	public static void setTextLayoutManager(TextLayoutManager textLayoutManager)
+	{
+		if (_textLayoutManager == null) {
+			_textLayoutManager = textLayoutManager;
+			TextLayout.setManager(textLayoutManager);
+		}
+	}
+
+	/** Gets the text-layout manager set for this session. */
+	public static TextLayoutManager getTextLayoutManager()
+	{
+		return _textLayoutManager;
+	}
+
+	/** Sets the input manager for this session. Can only be done once. */
+	public static void setInputManager(InputManager inputManager)
+	{
+		if (_inputManager == null) {
+			_inputManager = inputManager;
+			_inputManager.addInputEventToGestureTranslator(new ExpediteeKBMGestureTranslator());
+			_inputManager.registerGestureListener(StandardGestureActions.getInstance());
+			StandardInputEventListeners.registerWith(_inputManager);
+		}
+	}
+
+	/** Gets the input manager set for this session. */
+	public static InputManager getInputManager()
+	{
+		return _inputManager;
+	}
+
+	/** Sets the misc. manager for this session. Can only be done once. */
+	public static void setMiscManager(MiscManager miscManager)
+	{
+		if (_miscManager == null) {
+			_miscManager = miscManager;
+		}
+	}
+
+	/** Gets the misc. manager set for this session. */
+	public static MiscManager getMiscManager()
+	{
+		return _miscManager;
+	}
+
+	/** Sets the drag-and-drop manager for this session. Can only be done once. */
+	public static void setDragAndDropManager(DragAndDropManager dndManager)
+	{
+		if (_dndManager == null) {
+			_dndManager = dndManager;
+		}
+	}
+
+	/** Gets the drag-and-drop manager set for this session. */
+	public static DragAndDropManager getDragAndDropManager()
+	{
+		return _dndManager;
+	}
+}
Index: /trunk/src/org/expeditee/gio/FontManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/FontManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/FontManager.java	(revision 1097)
@@ -0,0 +1,59 @@
+package org.expeditee.gio;
+
+import java.io.File;
+
+import org.expeditee.core.Font;
+
+/**
+ * Class for managing font references in Expeditee. Expeditee's Font class is an
+ * opaque handle that is translated to an internal representation when used by
+ * other managers. The internal representation used for a given Font handle is
+ * implementation-specific (i.e. if an exact match for the font handle is not registered
+ * with the manager, the alternative selected is undefined).
+ * 
+ * TTF font files can be registered with the manager to increase the range of internal
+ * fonts available, and a collection of handles to all of the fonts the manager supports
+ * can be requested.
+ * 
+ * @author cts16
+ */
+public abstract class FontManager {
+
+	/** The default font registered with the manager. */
+	private Font _defaultFont = new Font();
+
+	/** Returns the default font registered with the manager. */
+	public final Font getDefaultFont()
+	{
+		return _defaultFont;
+	}
+
+	/** Sets the default font for the font manager. */
+	public final void setDefaultFont(final Font font)
+	{
+		_defaultFont = font;
+	}
+	
+	/** Registers a font file with the font manager, and returns the handle
+	    to the newly registered font. */
+	public abstract Font registerFontFile(File fontFile);
+	
+	/** Gets an array of all font families registered with the manager. */
+	public abstract String[] getRegisteredFontFamilies();
+	
+	/** Returns true if the given font is registered with the manager. */
+	public abstract boolean isFontFamilyRegistered(String fontFamily);
+
+	/** Returns a handle representing the actual font used for the given font. */
+	public abstract Font getActualFont(Font font);
+	
+	/** Checks if the given font was matched exactly by the font manager. */
+	public boolean fontIsActualFont(Font font)
+	{
+		if (font == null) return false;
+		
+		return font.equals(getActualFont(font));
+	}
+	
+	
+}
Index: /trunk/src/org/expeditee/gio/GraphicsManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/GraphicsManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/GraphicsManager.java	(revision 1097)
@@ -0,0 +1,308 @@
+package org.expeditee.gio;
+
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.expeditee.core.Clip;
+import org.expeditee.core.Colour;
+import org.expeditee.core.Cursor;
+import org.expeditee.core.Dimension;
+import org.expeditee.core.Fill;
+import org.expeditee.core.Font;
+import org.expeditee.core.Image;
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+import org.expeditee.core.Stroke;
+import org.expeditee.core.TextLayout;
+import org.expeditee.core.EnforcedClipStack.EnforcedClipKey;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.core.bounds.EllipticalBounds;
+import org.expeditee.core.bounds.PolygonBounds;
+import org.expeditee.items.widgets.Widget;
+
+/** TODO: Comment. cts16 */
+public abstract class GraphicsManager {
+
+	/** TODO: Comment. cts16 */
+	protected static String _windowIcon = "";
+	
+	protected GraphicsManager()
+	{
+		_widgets = new LinkedList<Widget>();
+	}
+
+	/*
+	 * 
+	 * Screen and window methods.
+	 * 
+	 */
+	
+	/** Gets the width and height of the display screen in pixels. */
+	public abstract Dimension getScreenSize();
+	
+	/**
+	 * Gets the pixel location of the top-left corner of the window drawing area
+	 * in screen-relative coordinates.
+	 */
+	public abstract Point getWindowLocation();
+	
+	/**
+	 * Sets the pixel location of the top-left corner of the window drawing area
+	 * in screen-relative coordinates.
+	 */
+	public abstract void setWindowLocation(Point p);
+
+	/** Gets the dimensions of window drawing area. */
+	public abstract Dimension getWindowSize();
+	
+	/** Sets the dimensions of window drawing area. */
+	public abstract void setWindowSize(Dimension d);
+	
+	/** Tries to focus the window. */
+	public abstract void requestFocus();
+	
+	/** Tells the GraphicsManager which icon to use for the window. */
+	public static final void setWindowIcon(String filename)
+	{
+		if (_windowIcon == "") _windowIcon = filename;
+	}
+	
+	/** Should be called by the platform-specific graphics manager to set the window's icon. */
+	protected void setWindowIcon()
+	{
+		// set up the image used for the icon
+	    try
+        {
+          URL iconURL = ClassLoader.getSystemResource(_windowIcon);
+          if (iconURL != null)
+          {
+            Image localImage = Image.getImage(iconURL);
+            setWindowIcon(localImage);
+          }
+        }
+        catch (Exception e)
+        {
+          e.printStackTrace();
+        }
+	}
+	
+	/** Sets the window's icon to the given image. */
+	protected abstract void setWindowIcon(Image image);
+	
+	/** Sets the title text for the window. */
+	public abstract void setWindowTitle(String title);
+	
+	/** Returns whether or not the window is capable of going fullscreen. */
+	public abstract boolean canGoFullscreen();
+	
+	/** Returns whether or not the window is currently in fullscreen mode. */
+	public abstract boolean isFullscreen();
+	
+	/** Tells the window to enter fullscreen mode. */
+	public abstract void goFullscreen();
+	
+	/** Tells the window to leave fullscreen mode. */
+	public abstract void exitFullscreen();
+	
+	/*
+	 * 
+	 * Drawing-surface methods.
+	 * 
+	 */
+	
+	/** Gets the platform-specific sub-typed surface stack. */
+	protected abstract GraphicsSurfaceStack<?> getGraphicsSurfaceStack();
+	
+	/** Puts the given image on top of the drawing stack, making it the current rendering surface. */
+	public void pushDrawingSurface(Image image)
+	{
+		getGraphicsSurfaceStack().push(image);
+	}
+	
+	/** Removes a rendering surface from the drawing stack. */
+	public Image popDrawingSurface()
+	{
+		return getGraphicsSurfaceStack().pop();
+	}
+	
+	/** Pushes a new clip onto the stack, ensuring that it is a sub-area of the top of the stack. */
+	public EnforcedClipKey pushClip(Clip clip)
+	{
+		return getGraphicsSurfaceStack().pushClip(clip);
+	}
+	
+	/** Pops the top of the current clip-stack if there given key corresponds to it. */
+	public Clip popClip(EnforcedClipKey key)
+	{
+		return getGraphicsSurfaceStack().popClip(key);
+	}
+	
+	/** Returns a copy of the top of the current clip-stack. */
+	public Clip peekClip()
+	{
+		return getGraphicsSurfaceStack().peekClip();
+	}
+	
+	/** Gets the size of the current drawing surface. */
+	public abstract Dimension getCurrentDrawingSurfaceSize();
+	
+	/** Sets the global compositing alpha value for the current rendering surface. */
+	public abstract void setCompositeAlpha(float alpha);
+	
+	/** Sets whether antialiased rendering should be used on the current surface. */
+	public abstract void setAntialiasing(boolean on);
+	
+	/** Sets the default font for the current rendering surface. */
+	public abstract void setFont(Font font);
+	
+	/*
+	 * 
+	 * Image-drawing methods.
+	 * 
+	 */
+	
+	/** Draws the given image at its natural size at the given location. */
+	public void drawImage(Image image, Point topLeft)
+	{
+		drawImage(image, topLeft, null);
+	}
+	
+	/** Draws the given image at the given size. */
+	public void drawImage(Image image, Point topLeft, Dimension size)
+	{
+		drawImage(image, topLeft, size, 0.0f);
+	}
+	
+	/** Draws the given image rotated about its top-left corner by the given angle. */
+	public void drawImage(Image image, Point topLeft, Dimension size, double angle)
+	{
+		drawImage(image, topLeft, size, angle, null, null);
+	}
+	
+	/** Draws the given cropped area of the given image. */
+	public abstract void drawImage(Image image, Point topLeft, Dimension size, double angle, Point cropTopLeft, Dimension cropSize);
+	
+	/*
+	 * 
+	 * Shape-drawing methods.
+	 * 
+	 */
+	
+	/** Completely fills the current surface with the specified colour. */
+	public void clear(Colour colour)
+	{
+		// Can't clear to no colour
+		if (colour == null) return;
+		
+		Dimension fullSurface = getCurrentDrawingSurfaceSize();
+		Fill fill = new Fill(colour);
+		
+		drawRectangle(new Point(0, 0), fullSurface, 0.0, fill, null, null, null);
+	}
+	
+	/** Draws the given rectangle. */
+	public void drawRectangle(AxisAlignedBoxBounds rect, double angle, Fill fill, Colour borderColour, Stroke borderStroke, Dimension cornerRadius)
+	{
+		if (rect == null) return;
+		
+		drawRectangle(rect.getTopLeft(), rect.getSize(), angle, fill, borderColour, borderStroke, cornerRadius);
+	}
+	
+	/** Draws a rectangle specified by the given top-left corner and dimensions. */
+	public abstract void drawRectangle(Point topLeft, Dimension size, double angle, Fill fill, Colour borderColour, Stroke borderStroke, Dimension cornerRadius);
+	
+	/** Draws the given ellipse. */
+	public void drawOval(EllipticalBounds oval, double angle, Fill fill, Colour borderColour, Stroke borderStroke)
+	{
+		if (oval == null) return;
+		
+		drawOval(oval.getCentre(), oval.getDiameters(), angle, fill, borderColour, borderStroke);
+	}
+	
+	/** Draws an oval defined by the given centre-point and diameters. */
+	public abstract void drawOval(Point centre, Dimension diameters, double angle, Fill fill, Colour borderColour, Stroke borderStroke);
+	
+	/** Draws a polygon. Fills the closed area but strokes the open line. */
+	public abstract void drawPolygon(PolygonBounds points, Point offset, Dimension scale, double angle, Fill fill, Colour borderColour, Stroke borderStroke);
+	
+	/** Draws the given line. */
+	public void drawLine(Line line, Colour colour, Stroke stroke)
+	{
+		if (line == null) return;
+		
+		drawLine(line.getFirstEnd(), line.getSecondEnd(), colour, stroke);
+	}
+	
+	/** Draws a line defined by two end points. */
+	public void drawLine(Point p1, Point p2, Colour colour, Stroke stroke)
+	{
+		if (p1 == null || p2 == null) return;
+		
+		drawLine(p1.x, p1.y, p2.x, p2.y, colour, stroke);
+	}
+	
+	/** Draws a line defined by two end points. */
+	public abstract void drawLine(int x1, int y1, int x2, int y2, Colour colour, Stroke stroke);
+	
+	/*
+	 * 
+	 * Text-drawing methods.
+	 * 
+	 */
+	
+	/** Draws the given string. */
+	public abstract void drawString(String string, Point position, Font font, Colour colour);
+	
+	/** Draws the text in the given text-layout. */
+	public void drawTextLayout(TextLayout layout, Point position, Colour colour)
+	{
+		drawString(layout.getLine(), position, layout.getFont(), colour);
+	}
+	
+	/*
+	 * 
+	 * Cursor-graphics methods.
+	 * 
+	 */
+
+	/** TODO: Comment. cts16 */
+	public abstract void setCursor(Cursor cursor);
+
+	/** TODO: Comment. cts16 */
+	public abstract Dimension getBestCursorSize(Dimension desiredSize);
+	
+	/*
+	 * 
+	 * Dialog box methods.
+	 * 
+	 */
+
+	/** TODO: Comment. cts16 */
+	public abstract boolean showDialog(String title, String message);
+	
+	/*
+	 * 
+	 * Widget methods.
+	 * 
+	 */
+	
+	/** TODO: Comment. cts16 */
+	protected List<Widget> _widgets;
+	
+	/** TODO: Comment. cts16 */
+	public boolean addInteractiveWidget(Widget iw)
+	{
+		if (iw == null) return false;
+		
+		return _widgets.add(iw);
+	}
+	
+	/** TODO: Comment. cts16 */
+	public boolean removeInteractiveWidget(Widget iw)
+	{
+		if (iw == null) return false;
+		
+		return _widgets.remove(iw);
+	}
+}
Index: /trunk/src/org/expeditee/gio/GraphicsSurfaceStack.java
===================================================================
--- /trunk/src/org/expeditee/gio/GraphicsSurfaceStack.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/GraphicsSurfaceStack.java	(revision 1097)
@@ -0,0 +1,139 @@
+package org.expeditee.gio;
+
+import java.util.Stack;
+
+import org.expeditee.core.Clip;
+import org.expeditee.core.EnforcedClipStack;
+import org.expeditee.core.Image;
+import org.expeditee.core.EnforcedClipStack.EnforcedClipKey;
+
+/**
+ * Helper-class for the graphics manager to maintain a stack of drawing
+ * surfaces. Should be sub-typed by the platform-specific graphics manager
+ * to use the appropriate surface class.
+ */
+public abstract class GraphicsSurfaceStack<T> {
+
+	/** The root surface corresponds to drawing directly into the window. */
+	private T _rootSurface;
+	/** The surface that drawing commands should draw into. */
+	private T _currentSurface;
+	/** The stack of drawing surfaces. */
+	private Stack<T> _surfaceStack;
+	/** The image that produced the current drawing surface. */
+	private Image _currentImage;
+	/** The stack of images that produced the surfaces in the surface stack. */
+	private Stack<Image> _imageStack;
+	/** The clip-stack for the root surface. */
+	private EnforcedClipStack _rootClipStack;
+	/** The clip-stack for the current surface. */
+	private EnforcedClipStack _currentClipStack;
+	/** The clip-stack for each drawing surface. */
+	private Stack<EnforcedClipStack> _clipStackStack;
+	
+	/** Constructor specifying no root surface. */
+	public GraphicsSurfaceStack()
+	{
+		this(null);
+	}
+	
+	/** Constructor specifying the root surface. */
+	public GraphicsSurfaceStack(T rootSurface)
+	{
+		setRootSurface(rootSurface);
+		_currentImage = null;
+		_surfaceStack = new Stack<T>();
+		_imageStack = new Stack<Image>();
+		_rootClipStack = _currentClipStack = new EnforcedClipStack();
+		_clipStackStack = new Stack<EnforcedClipStack>();
+	}
+	
+	/** Should be overridden by sub-types to allow a drawing surface to be extracted from an image. */
+	public abstract T getSurfaceFromImage(Image image);
+	
+	/** Should be overridden by sub-types to set the actual clip on a given surface. */
+	public abstract void setSurfaceClip(T surface, Clip clip);
+	
+	/** Pushes a new image onto the stack, and sets it as the current drawing surface. */
+	public void push(Image image)
+	{
+		if (image == null) return;
+		
+		_currentSurface = getSurfaceFromImage(image);
+		_currentImage = image;
+		_currentClipStack = new EnforcedClipStack();
+		_surfaceStack.push(_currentSurface);
+		_imageStack.push(image);
+		_clipStackStack.push(_currentClipStack);
+	}
+	
+	/** Removes an image from the stack. The removed image is returned, or null if the root surface was current. */
+	public Image pop()
+	{
+		if (_surfaceStack.isEmpty()) return null;
+		
+		Image oldTop = _imageStack.pop();
+		_surfaceStack.pop();
+		_clipStackStack.pop();
+		
+		if (!_surfaceStack.isEmpty()) {
+			_currentSurface = _surfaceStack.peek();
+			_currentImage = _imageStack.peek();
+			_currentClipStack = _clipStackStack.peek();
+		} else {
+			_currentSurface = _rootSurface;
+			_currentImage = null;
+			_currentClipStack = _rootClipStack;
+		}
+		
+		return oldTop;
+	}
+	
+	/** Gets a reference to the current drawing surface. */
+	public T getCurrentSurface()
+	{
+		return _currentSurface;
+	}
+	
+	/** Gets a reference to the top image on the stack. */
+	public Image getCurrentImage()
+	{
+		return _currentImage;
+	}
+	
+	/** Pushes a new clip onto the stack, ensuring that it is a sub-area of the top of the stack. */
+	public EnforcedClipKey pushClip(Clip clip)
+	{
+		EnforcedClipKey key = _currentClipStack.push(clip);
+		if (key != null) ensureClip();
+		return key;
+	}
+	
+	/** Pops the top of the current clip-stack if there given key corresponds to it. */
+	public Clip popClip(EnforcedClipKey key)
+	{
+		Clip clip = _currentClipStack.pop(key);
+		if (clip != null) ensureClip();
+		return clip;
+	}
+	
+	/** Returns a copy of the top of the current clip-stack. */
+	public Clip peekClip()
+	{
+		return _currentClipStack.peek();
+	}
+	
+	/** Ensures the current clip is applied to the underlying surface. */
+	public void ensureClip()
+	{
+		setSurfaceClip(_currentSurface, _currentClipStack.peek());
+	}
+	
+	/** Updates the root surface. Does not affect any stacked image surfaces. */
+	public void setRootSurface(T rootSurface)
+	{
+		if (_currentSurface == _rootSurface) _currentSurface = rootSurface;
+		_rootSurface = rootSurface;
+		
+	}
+}
Index: /trunk/src/org/expeditee/gio/ImageManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/ImageManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/ImageManager.java	(revision 1097)
@@ -0,0 +1,129 @@
+package org.expeditee.gio;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.expeditee.core.Colour;
+import org.expeditee.core.Dimension;
+import org.expeditee.core.Image;
+
+import com.sun.pdfview.PDFPage;
+
+/**
+ * Manager in charge of creating and working with images in memory. Maintains a mapping
+ * from org.expeditee.core.Image handles to the underlying images, and provides methods
+ * for manipulating the underlying images through those handles.
+ * 
+ * @author cts16
+ */
+public abstract class ImageManager {
+	
+	/*
+	 * 
+	 *  Image-creation functions
+	 *  
+	 */
+	
+	/**
+	 * Creates an image of the given size in either main or video RAM.
+	 * 
+	 * @param hintUseAcceleratedMemory
+	 * 			True to use video RAM, false for main memory.
+	 */
+	public Image createImage(Dimension size, boolean hintUseAcceleratedMemory)
+	{
+		if (size == null) return null;
+		
+		return createImage(size.width, size.height, hintUseAcceleratedMemory);
+	}
+	
+	/**
+	 * Creates an image of the given width and height in either main or video RAM.
+	 * 
+	 * @param hintUseAcceleratedMemory
+	 * 			True to use video RAM, false for main memory.
+	 */
+	public abstract Image createImage(int width, int height, boolean hintUseAcceleratedMemory);
+
+	/** Creates an image in main memory with the given size. */
+	public Image createImage(Dimension size)
+	{
+		return createImage(size, false);
+	}
+
+	/** Creates an image in main memory with the given width and height. */
+	public Image createImage(int width, int height)
+	{
+		return createImage(width, height, false);
+	}
+	
+	/** Creates an image from packed 32-bit ARGB data in an array. */
+	public abstract Image createImage(int width, int height, int[] pixelData);
+	
+	/** Creates an image of a PDF page. */
+	public abstract Image createImage(int width, int height, PDFPage page);
+	
+	/** Creates a new image which is a cropped section of another image. */
+	public abstract Image createImageAsCroppedCopy(Image orig, int x, int y, int width, int height);
+	
+	/** Gets image data from a URL. */
+	public abstract Image getImage(URL url);
+	
+	/** Gets image data from a file. */
+	public abstract Image getImage(String filename);
+	
+	/** Gets image data from a HTTP connection. */
+	public abstract Image getImage(HttpURLConnection connection) throws IOException;
+	
+	/*
+	 * 
+	 *  Image-destruction functions
+	 *  
+	 */
+	
+	/** Tells the image manager that the given image is no longer needed. */
+	public abstract void releaseImage(Image image);
+	
+	/*
+	 * 
+	 *  Image info getters
+	 *  
+	 */
+	
+	/** Checks if the given handle corresponds to a real image in the image manager. */
+	public abstract boolean isImageValid(Image image);
+	
+	/** Gets the width of the given image. */
+	public abstract int getWidth(Image image);
+	
+	/** Gets the height of the given image. */
+	public abstract int getHeight(Image image);
+	
+	/** Gets the colours of an area of pixels from the image. */
+	public abstract Colour[] getPixels(Image image, int x, int y, int width, int height);
+	
+	/** Gets the colour of a single pixel in the image. */
+	public Colour getPixel(Image image, int x, int y)
+	{
+		Colour[] pixels = getPixels(image, x, y, 1, 1);
+		
+		if (pixels == null) return null;
+		
+		return pixels[0];
+	}
+	
+	/*
+	 * 
+	 *  Image-manipulation functions
+	 *  
+	 */
+	
+	/** Sets the colour of a single pixel in the image. */
+	public abstract void setPixel(Image image, int x, int y, Colour c);
+	
+	/** Writes the image data to disk. Format should be png, bmp, etc. */
+	public abstract boolean writeImageToDisk(Image image, String format, File file) throws IOException;
+	
+}
Index: /trunk/src/org/expeditee/gio/InputManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/InputManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/InputManager.java	(revision 1097)
@@ -0,0 +1,387 @@
+package org.expeditee.gio;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import org.expeditee.core.Point;
+import org.expeditee.gio.TimeoutQueue.TimeoutHandle;
+import org.expeditee.gio.gesture.Gesture;
+import org.expeditee.gio.gesture.GestureListener;
+import org.expeditee.gio.input.InputEvent;
+import org.expeditee.gio.input.InputEventListener;
+import org.expeditee.gio.input.InputEventToGestureTranslator;
+import org.expeditee.gio.input.InputEvent.InputType;
+import org.expeditee.gio.input.TimeoutInputEvent;
+import org.expeditee.items.widgets.Widget;
+
+/**
+ * Receives input events from the system and distributes them within Expeditee.
+ * Abstract as platform-specific implementation must be provided to handle receiving
+ * input events from the system.
+ * 
+ *                  Native Input
+ *                       |
+ *                       |        /---------\
+ *                       `------->| Widgets |
+ *                       ,--------|         |
+ *                       |        \---------/
+ *                       v
+ *                /-------------\
+ *                | Input       |
+ *                | Translation |
+ *                \-------------/
+ *                       |
+ *                 Expeditee Input
+ *                       |
+ *                       |        /-----------\
+ *                       `------->| Input     |
+ *                       ,--------| Listeners |
+ *                       |        \-----------/
+ *                       |
+ *                       v
+ *                /-------------\
+ *    Context---->| Gesture     |
+ *                | Translation |
+ *                \-------------/
+ *                       |
+ *                    Gestures
+ *                       |        /-----------\
+ *                       `------->| Gesture   |
+ *                                | Listeners |
+ *                                \-----------/
+ * 
+ * @author cts16
+ */
+public abstract class InputManager {
+	
+	protected InputManager()
+	{
+		// Initialise the list of window-event listeners 
+		_windowEventListeners = new LinkedList<WindowEventListener>();
+		
+		// Initialise the timeout queue
+		_timeoutQueue = new TimeoutQueue();
+		
+		// Initialise the list of widgets
+		_widgets = new LinkedList<Widget>();
+		
+		// Initialise the list of input event listeners
+		_inputEventListeners = new LinkedList<InputEventListener>();
+		
+		// Initialise the map of input-event -> gesture translators
+		_translators = new HashMap<InputType, List<InputEventToGestureTranslator>>(InputType.values().length);
+		for (InputType type : InputType.values()) _translators.put(type, new LinkedList<InputEventToGestureTranslator>());
+		
+		// Initialise the list of gesture listeners
+		_gestureListeners = new LinkedList<GestureListener>();
+		
+		// Initialise the cursor position
+		_cursorPosition = getRealCursorPosition().clone();
+	}
+	
+	/*
+	 * 
+	 * Window-specific events.
+	 * 
+	 */
+	
+	/** The types of window events that can be listened for. */
+	public enum WindowEventType {
+		WINDOW_RESIZED,
+		WINDOW_CLOSED,
+		MOUSE_EXITED_WINDOW,
+		MOUSE_ENTERED_WINDOW
+	}
+	
+	/** The interface window-event listeners must implement to receive events. */
+	public static interface WindowEventListener
+	{
+		public void onWindowEvent(WindowEventType type);
+	}
+	
+	/** The list of window-event listeners. */
+	private List<WindowEventListener> _windowEventListeners;
+	
+	/** Adds a new window-event listener. */
+	public void addWindowEventListener(WindowEventListener listener)
+	{
+		_windowEventListeners.add(listener);
+	}
+	
+	/**
+	 * Should be called by platform-specific input managers to
+	 * notify listeners of a window event.
+	 */
+	protected void distributeWindowEvent(WindowEventType type)
+	{
+		ListIterator<WindowEventListener> it = _windowEventListeners.listIterator(_windowEventListeners.size());
+		
+		// Run event handlers in reverse order
+		while (it.hasPrevious()) it.previous().onWindowEvent(type);
+	}
+	
+	/*
+	 * 
+	 * Cursor-control methods.
+	 * 
+	 */
+
+	/** The position of the mouse cursor. */
+	private Point _cursorPosition;
+	
+	/** Gets the current position of the cursor inside the Expeditee window. */
+	public Point getCursorPosition()
+	{
+		return _cursorPosition.clone();
+	}
+	
+	/** Gets the position of the cursor (for initialisation purposes). */
+	protected abstract Point getRealCursorPosition();
+	
+	/** Should be called by the platform-specific input manager to keep the cursor position up-to-date. */
+	protected void updateCursorPosition(int x, int y)
+	{
+		_cursorPosition.set(x, y);
+	}
+	
+	/** Moves the cursor to the given position in the window. */
+	public abstract void setCursorPosition(Point position);
+	
+	/** Moves the cursor by the given amount. */
+	public final void moveCursor(int x, int y)
+	{
+		setCursorPosition(getCursorPosition().clone().add(x, y));
+	}
+
+	/*
+	 * 
+	 * Timeout-event handling.
+	 * 
+	 */
+	
+	/** TODO: Comment. cts16 */
+	private TimeoutQueue _timeoutQueue;
+
+	/** TODO: Comment. cts16 */
+	public TimeoutHandle addTimeout(int milliseconds)
+	{
+		TimeoutHandle handle = _timeoutQueue.addTimeout(milliseconds);
+		
+		Long nextTimeout = _timeoutQueue.getNextTimeout();
+		
+		if (nextTimeout != null) updateTimer(nextTimeout);
+		
+		return handle;
+	}
+
+	/** TODO: Comment. cts16 */
+	public void cancelTimeout(TimeoutHandle handle)
+	{
+		_timeoutQueue.cancelTimeout(handle);
+	}
+
+	/** TODO: Comment. cts16 */
+	protected abstract void updateTimer(long nextTimeout);
+
+	/** TODO: Comment. cts16 */
+	public final void triggerTimeoutEvents()
+	{
+		Set<TimeoutHandle> expiredHandles;
+		while (!(expiredHandles = _timeoutQueue.popExpiredTimeouts()).isEmpty()) {
+			triggerTimeoutEvents(expiredHandles);
+		}
+		
+		Long nextTimeout = _timeoutQueue.getNextTimeout();
+		
+		if (nextTimeout != null) updateTimer(nextTimeout);
+	}
+
+	/** TODO: Comment. cts16 */
+	protected final void triggerTimeoutEvents(Collection<TimeoutHandle> handles)
+	{
+		if (handles == null) return;
+		
+		for (TimeoutHandle handle : handles) triggerTimeoutEvent(handle);
+	}
+
+	/** TODO: Comment. cts16 */
+	protected final void triggerTimeoutEvent(TimeoutHandle handle)
+	{
+		if (handle == null) return;
+		
+		TimeoutInputEvent event = new TimeoutInputEvent(handle);
+		
+		distributeInputEvent(event);
+	}
+	
+	/*
+	 * 
+	 * Native-input-layer distribution methods.
+	 * 
+	 */
+
+	/** The widgets enrolled with the input manager. */
+	protected List<Widget> _widgets;
+	
+	/** Registers a widget with the manager. */
+	public void addInteractiveWidget(Widget iw)
+	{
+		if (iw == null) return;
+		
+		if (!_widgets.contains(iw)) _widgets.add(iw);
+	}
+	
+	/** Unregisters a widget with the manager. */
+	public void removeInteractiveWidget(Widget iw)
+	{
+		if (iw == null) return;
+		
+		if (_widgets.contains(iw)) _widgets.remove(iw);
+	}
+
+	/*
+	 * 
+	 * Expeditee-input-layer distribution methods.
+	 * 
+	 */
+	
+	/** The registered input event listeners. */
+	private LinkedList<InputEventListener> _inputEventListeners;
+	
+	/**
+	 * Registers the given input event listener to intercept input events.
+	 * Priority of interception is in order of registration.
+	 */
+	public final void registerInputEventListener(InputEventListener listener)
+	{
+		if (listener == null) return;
+		
+		_inputEventListeners.add(listener);
+	}
+	
+	/** Unregisters an input event listener. */
+	public final void removeInputEventListener(InputEventListener listener)
+	{
+		if (listener == null) return;
+		
+		_inputEventListeners.remove(listener);
+	}
+	
+	/**
+	 * Lets any registered input-event listeners view/consume input. If none
+	 * consume the input, it gets passed to the gesture-translation system. This method
+	 * should be called by the implementation-native input system to get input into
+	 * Expeditee.
+	 */
+	protected final void distributeInputEvent(InputEvent event)
+	{
+		// Allow listeners to optionally consume input before gesture translation
+		for (InputEventListener listener : _inputEventListeners) {
+			if (listener.onInputEvent(event)) return;
+		}
+		
+		// Get the relevant translators
+		List<InputEventToGestureTranslator> translators = getInputEventToGestureTranslators(event.getInputType());
+		
+		// Translate the input event in reverse order, only taking the gestures from the first
+		// translator to respond
+		List<Gesture> gestures = null;
+		ListIterator<InputEventToGestureTranslator> it = translators.listIterator(translators.size());
+		while (it.hasPrevious()) {
+			InputEventToGestureTranslator translator = it.previous();
+			List<Gesture> translatorGestures = translator.onInputEvent(event);
+			if (gestures == null && translatorGestures != null && !translatorGestures.isEmpty()) gestures = translatorGestures;
+		}
+		
+		// Perform the gestures
+		distributeGestures(gestures, false);
+	}
+	
+	/*
+	 * 
+	 * Gesture-layer distribution methods.
+	 * 
+	 */
+	
+	/** The registered InputEventToGestureTranslators. */
+	private HashMap<InputType, List<InputEventToGestureTranslator>> _translators;
+	
+	/** The registered gesture listeners. */
+	private LinkedList<GestureListener> _gestureListeners;
+	
+	/**
+	 * Sets the translator to use to generate gestures for a given type of input.
+	 * @return True if the input type is supported by this Input Manager, false if not.
+	 */
+	public final void addInputEventToGestureTranslator(InputEventToGestureTranslator translator)
+	{
+		if (translator == null) return;
+		
+		Set<InputType> types = translator.getMonitoredInputTypes();
+		
+		for (InputType type : types) _translators.get(type).add(translator);
+	}
+	
+	/**
+	 * Gets the input-event -> gesture translator that was registered with the
+	 * given input type.
+	 */
+	public final List<InputEventToGestureTranslator> getInputEventToGestureTranslators(InputType type)
+	{
+		if (type == null) return null;
+		
+		return _translators.get(type);
+	}
+	
+	/**
+	 * Should return true if the input manager supports the given
+	 * type of input and false if not.
+	 */
+	protected abstract boolean isInputTypeSupported(InputType type);
+
+	/** Registers a new listener to receive gestures from the input system. */
+	public final void registerGestureListener(GestureListener listener)
+	{
+		if (listener == null) return;
+		
+		_gestureListeners.add(listener);
+	}
+	
+	/** Notifies all gesture listeners of the given gestures. */
+	public void distributeGestures(List<Gesture> gestures)
+	{
+		distributeGestures(gestures, true);
+	}
+
+	/** Notifies all gesture listeners of the given gestures. */
+	protected void distributeGestures(List<Gesture> gestures, boolean robotic)
+	{
+		if (gestures == null) return;
+		
+		for (Gesture gesture : gestures) distributeGesture(gesture, robotic);
+	}
+
+	/** Notifies all gesture listeners of the given gesture. */
+	public void distributeGesture(Gesture gesture)
+	{
+		distributeGesture(gesture, true);
+	}
+
+	/** Notifies all gesture listeners of the given gesture. */
+	protected void distributeGesture(Gesture gesture, boolean robotic)
+	{
+		if (gesture == null) return;
+		
+		// Ensure robotic callers don't try to spoof being non-robotic
+		if (robotic) gesture.setRobotic(true);
+
+		for (GestureListener listener : _gestureListeners) {
+			listener.onGesture(gesture.clone());
+		}
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/MiscManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/MiscManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/MiscManager.java	(revision 1097)
@@ -0,0 +1,63 @@
+package org.expeditee.gio;
+
+import org.expeditee.core.BlockingRunnable;
+
+/** TODO: Comment. cts16 */
+public abstract class MiscManager {
+
+	/**
+	 * Causes the machine the interface is running on to make a "beep" sound.
+	 */
+	public abstract void beep();
+	
+	/** TODO: Comment. cts16 */
+	public abstract boolean browse(String url);
+	
+	/** TODO: Comment. cts16 */
+	public abstract String print(String file);
+	
+	/** TODO: Comment. cts16 */
+	public abstract String open(String file);
+	
+	/**
+	 * Runs the given routine on the dedicated GIO event thread, if the GIO system has one.
+	 * 
+	 * @param routine
+	 * 					Should be a BlockingRunnable if this method should block until
+	 * 					the routine has finished, or an ordinary runnable if not.
+	 */
+	public void runOnGIOThread(Runnable routine)
+	{
+		// Can't run nothing
+		if (routine == null) return;
+		
+		// If given a blocking runnable, don't return until it has executed
+		if (routine instanceof BlockingRunnable) {
+			// If we are the GIO thread, can run now
+			if (isGIOThread()) {
+				routine.run();
+				
+			// Otherwise schedule and block
+			} else {
+				scheduleRunnable(routine);
+				((BlockingRunnable) routine).blockUntilDone();
+			}
+			
+		// If not a blocking runnable, just schedule the routine for later and return
+		} else {
+			scheduleRunnable(routine);
+		}
+	}
+	
+	/**
+	 * Schedules a runnable to be executed at some future time on the GIO thread.
+	 */
+	protected abstract void scheduleRunnable(Runnable routine);
+	
+	/**
+	 * Returns true if the calling thread is the dedicated GIO thread,
+	 * or false if any other thread.
+	 */
+	protected abstract boolean isGIOThread();
+	
+}
Index: /trunk/src/org/expeditee/gio/TextLayoutManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/TextLayoutManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/TextLayoutManager.java	(revision 1097)
@@ -0,0 +1,95 @@
+package org.expeditee.gio;
+
+import java.util.List;
+
+import org.expeditee.core.Font;
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+import org.expeditee.core.TextHitInfo;
+import org.expeditee.core.TextLayout;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+
+/** TODO: Comment. cts16 */
+public abstract class TextLayoutManager {
+
+	public abstract List<TextLayout> layoutString(String string, Font font, Point start, Line[] lineBreakers, int widthLimit, int lineSpacing, boolean dontBreakWords, boolean fullJustify);
+	
+	public List<TextLayout> layoutStringRelative(String string, Font font, int widthLimit, int lineSpacing, boolean dontBreakWords, boolean fullJustify)
+	{
+		return layoutString(string, font, Point.ORIGIN, null, widthLimit, lineSpacing, dontBreakWords, fullJustify);
+	}
+	
+	public TextLayout layoutStringSimple(String string, Font font)
+	{
+		List<TextLayout> layouts = layoutStringRelative(string, font, Integer.MAX_VALUE, 0, true, false);
+		if (layouts == null || layouts.size() == 0) return null;
+		return layouts.get(0);
+	}
+	
+	public abstract float getAdvance(TextLayout layout);
+	public abstract int getCharacterCount(TextLayout layout);
+	public abstract TextHitInfo getNextLeftHit(TextLayout layout, int offset);
+	public abstract TextHitInfo getNextLeftHit(TextLayout layout, TextHitInfo hit);
+	public abstract TextHitInfo getNextRightHit(TextLayout layout, int offset);
+	public abstract TextHitInfo getNextRightHit(TextLayout layout, TextHitInfo hit);
+	public abstract float[] getCaretInfo(TextLayout layout, TextHitInfo hit);
+	public abstract AxisAlignedBoxBounds getPixelBounds(TextLayout layout, float x, float y);
+	public abstract AxisAlignedBoxBounds getLogicalHighlightShape(TextLayout layout, int firstEndpoint, int secondEndpoint);
+	public abstract float getAscent(TextLayout layout);
+	public abstract float getDescent(TextLayout layout);
+	public abstract float getLeading(TextLayout layout);
+	public abstract int getStringWidth(Font font, String string);
+	public abstract TextHitInfo hitTestChar(TextLayout layout, float x, float y);
+	
+	public abstract void releaseLayout(TextLayout layout);
+	
+	public void releaseLayouts(List<TextLayout> layouts)
+	{
+		if (layouts == null) return;
+		
+		for (TextLayout layout : layouts) {
+			releaseLayout(layout);
+		}
+	}
+	
+	/**
+	 * Calculates the maximum possible distance a line can extend to the right from a given point
+	 * without crossing any of the lines in the given array.
+	 * 
+	 * @param p
+	 * 		The coordinates of the beginning point.
+	 * 
+	 * @param lines
+	 * 		The lines that should stop the extension.
+	 * 
+	 * @return
+	 * 		The length of the extended line.
+	 */
+	public float getLineWidth(Point p, Line[] lines)
+	{
+		float width = Float.MAX_VALUE;
+		for(Line l : lines) {
+			// check for lines that cross over our y
+			if((l.getFirstEnd().y >= p.y && l.getSecondEnd().y <= p.y) || (l.getFirstEnd().y <= p.y && l.getSecondEnd().y >= p.y)) {
+				float dX = l.getFirstEnd().x - l.getSecondEnd().x;
+				float dY = l.getFirstEnd().y - l.getSecondEnd().y;
+				float newWidth;
+				if(dX == 0) {
+					newWidth = l.getFirstEnd().x;
+				} else if(dY == 0) {
+					newWidth = Math.min(l.getFirstEnd().x, l.getSecondEnd().x);
+				} else {
+					newWidth = l.getFirstEnd().x + (p.y - l.getFirstEnd().y) * dX / dY;
+				}
+				if(newWidth < p.x) {
+				 	continue;
+				}
+				if(newWidth < width) {
+					width = newWidth;
+				}
+			}
+		}
+		return width - p.x;
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/TimeoutQueue.java
===================================================================
--- /trunk/src/org/expeditee/gio/TimeoutQueue.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/TimeoutQueue.java	(revision 1097)
@@ -0,0 +1,150 @@
+package org.expeditee.gio;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.PriorityQueue;
+
+public class TimeoutQueue {
+	
+	private PriorityQueue<Long> _timeoutQueue;
+	
+	private HashSet<TimeoutHandle> _cancelledTimeouts;
+	
+	private HashMap<TimeoutHandle, Long> _timeoutHandleMap;
+	
+	private HashMap<Long, HashSet<TimeoutHandle>> _timeoutMap;
+	
+	public TimeoutQueue()
+	{
+		_timeoutQueue = new PriorityQueue<Long>();
+		_cancelledTimeouts = new HashSet<TimeoutHandle>();
+		_timeoutHandleMap = new HashMap<TimeoutHandle, Long>();
+		_timeoutMap = new HashMap<Long, HashSet<TimeoutHandle>>();
+	}
+	
+	public TimeoutHandle addTimeout(long milliseconds)
+	{
+		if (milliseconds < 0) return null;
+		
+		milliseconds += System.currentTimeMillis();
+		
+		TimeoutHandle handle = new TimeoutHandle();
+		
+		_timeoutQueue.add(milliseconds);
+		_timeoutHandleMap.put(handle, milliseconds);
+		if (!_timeoutMap.containsKey(milliseconds)) {
+			_timeoutMap.put(milliseconds, new HashSet<TimeoutHandle>());
+		}
+		_timeoutMap.get(milliseconds).add(handle);
+		
+		return handle;
+	}
+	
+	public void cancelTimeout(TimeoutHandle handle)
+	{
+		if (handle == null) return;
+		
+		if (!_timeoutHandleMap.containsKey(handle)) return;
+		
+		_cancelledTimeouts.add(handle);
+	}
+	
+	public Long getNextTimeout()
+	{
+		while (!_timeoutQueue.isEmpty()) {
+			long nextTimeout = _timeoutQueue.peek();
+			HashSet<TimeoutHandle> handlesForNextTimeout = handlesForTimeout(nextTimeout);
+			if (handlesAreCancelled(handlesForNextTimeout)) {
+				_timeoutQueue.poll();
+				_timeoutMap.remove(nextTimeout);
+				_cancelledTimeouts.removeAll(handlesForNextTimeout);
+				for (TimeoutHandle handle : handlesForNextTimeout) _timeoutHandleMap.remove(handle);
+			} else {
+				return nextTimeout - System.currentTimeMillis();
+			}
+		}
+		
+		// No valid timeout found
+		return null;
+	}
+	
+	public HashSet<TimeoutHandle> popExpiredTimeouts()
+	{
+		HashSet<TimeoutHandle> ret = new HashSet<TimeoutHandle>();
+		
+		while (!_timeoutQueue.isEmpty()) {
+			long nextTimeout = _timeoutQueue.peek();
+			long nextTimeoutRelative = nextTimeout - System.currentTimeMillis();
+			if (nextTimeoutRelative > 0) break;
+			HashSet<TimeoutHandle> handlesForNextTimeout = popNextTimeout();
+			ret.addAll(handlesForNextTimeout);
+		}
+		
+		return ret;
+	}
+	
+	private boolean handlesAreCancelled(Collection<TimeoutHandle> handles)
+	{
+		if (handles == null) return false;
+		
+		return _cancelledTimeouts.containsAll(handles);
+	}
+	
+	private boolean handleIsCancelled(TimeoutHandle handle)
+	{
+		if (handle == null) return false;
+		
+		return _cancelledTimeouts.contains(handle);
+	}
+	
+	private HashSet<TimeoutHandle> handlesForTimeout(long timeout)
+	{
+		if (!_timeoutMap.containsKey(timeout)) return null;
+		
+		return _timeoutMap.get(timeout);
+	}
+	
+	private HashSet<TimeoutHandle> popNextTimeout()
+	{
+		if (_timeoutQueue.isEmpty()) return null;
+		
+		long nextTimeout = _timeoutQueue.poll();
+		HashSet<TimeoutHandle> handlesForNextTimeout = handlesForTimeout(nextTimeout);
+		_timeoutMap.remove(nextTimeout);
+		for (TimeoutHandle handle : handlesForNextTimeout) {
+			if (handleIsCancelled(handle)) {
+				handlesForNextTimeout.remove(handle);
+				_cancelledTimeouts.remove(handle);
+			}
+			_timeoutHandleMap.remove(handle);
+		}
+		
+		return handlesForNextTimeout;
+	}
+	
+	public static class TimeoutHandle {
+
+		private static long _nextHandle = 1;
+
+		private static long getNextHandle()
+		{
+			long ret = _nextHandle;
+			_nextHandle++;
+			return ret;
+		}
+		
+		private long _handle;
+		
+		public TimeoutHandle()
+		{
+			_handle = getNextHandle();
+		}
+		
+		public long getHandle()
+		{
+			return _handle;
+		}
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/gesture/ExpediteeKBMGestureTranslator.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/ExpediteeKBMGestureTranslator.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/ExpediteeKBMGestureTranslator.java	(revision 1097)
@@ -0,0 +1,2060 @@
+package org.expeditee.gio.gesture;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.expeditee.core.Point;
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.TimeoutQueue.TimeoutHandle;
+import org.expeditee.gio.gesture.StandardGestureActions.StandardGestureType;
+import org.expeditee.gio.gesture.data.ChangeColourGestureData;
+import org.expeditee.gio.gesture.data.ClickGestureData;
+import org.expeditee.gio.gesture.data.CreateItemGestureData;
+import org.expeditee.gio.gesture.data.CreateItemGestureData.ItemType;
+import org.expeditee.gio.gesture.data.DeleteGestureData;
+import org.expeditee.gio.gesture.data.FormatGestureData;
+import org.expeditee.gio.gesture.data.GestureData;
+import org.expeditee.gio.gesture.data.InsertStringGestureData;
+import org.expeditee.gio.gesture.data.ItemSpecificGestureData;
+import org.expeditee.gio.gesture.data.JustifyGestureData;
+import org.expeditee.gio.gesture.data.LinkGestureData;
+import org.expeditee.gio.gesture.data.SelectAreaGestureData;
+import org.expeditee.gio.gesture.data.NavigateFrameGestureData;
+import org.expeditee.gio.gesture.data.NavigateTextGestureData;
+import org.expeditee.gio.gesture.data.PanGestureData;
+import org.expeditee.gio.gesture.data.PickUpGestureData;
+import org.expeditee.gio.gesture.data.RefreshGestureData;
+import org.expeditee.gio.gesture.data.ScaleGestureData;
+import org.expeditee.gio.gesture.data.UndoGestureData;
+import org.expeditee.gio.gesture.data.ZoomGestureData;
+import org.expeditee.gio.input.InputEvent;
+import org.expeditee.gio.input.InputEventToGestureTranslator;
+import org.expeditee.gio.input.InputEventToGestureTranslatorSet;
+import org.expeditee.gio.input.KBMInputEvent;
+import org.expeditee.gio.input.StandardInputEventListeners;
+import org.expeditee.gio.input.InputEvent.InputType;
+import org.expeditee.gio.input.KBMInputEvent.EventType;
+import org.expeditee.gio.input.KBMInputEvent.Key;
+import org.expeditee.gio.input.KBMInputEvent.MouseButton;
+import org.expeditee.gio.input.TimeoutInputEvent;
+import org.expeditee.gui.DisplayController;
+import org.expeditee.gui.FrameUtils;
+import org.expeditee.gui.FreeItems;
+import org.expeditee.gui.MessageBay;
+import org.expeditee.items.Item;
+import org.expeditee.items.Line;
+import org.expeditee.items.Text;
+import org.expeditee.settings.UserSettings;
+import org.expeditee.settings.experimental.ExperimentalFeatures;
+
+/**
+ * Class for translating raw keyboard and mouse input into Expeditee gestures.
+ * 
+ * @author cts16
+ */
+public class ExpediteeKBMGestureTranslator extends InputEventToGestureTranslatorSet
+{
+	/** Convenience definition of this translator's input type. */
+	private static final InputType KBM = InputType.KBM;
+	
+	/** Tracks when the mouse has moved too far away from it's click position and should no-op. */
+	private static boolean _noop = false;
+	/** Tracks the starting position of a mouse drag. */
+	private static Point _mouseDragStart;
+	
+	public ExpediteeKBMGestureTranslator()
+	{
+		// Create the list of sub-translators
+		initialiseExpediteeTranslators();
+	}
+	
+	@Override
+	public List<Gesture> onInputEvent(InputEvent event)
+	{
+		// Track when the mouse has moved too far and should no-op
+		if (event.getInputType() == KBM) trackMouseDragNoop((KBMInputEvent) event);
+		
+		return super.onInputEvent(event);
+	}
+	
+	/** Adds all gesture translators that base Expeditee utilises. */
+	private void initialiseExpediteeTranslators()
+	{
+		// TODO: Remove. Reference automatically falls through. cts16
+		ExpediteeKBMGestureTranslator kbmTranslator = this;
+		
+		// Add the CHAR_TYPED -> INSERT_STRING gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CHAR_TYPED -> INSERT_STRING"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check if it's a typing event
+				if (kbmEvent.getEventType() != KBMInputEvent.EventType.CHAR_TYPED) return null;
+				
+				// Generate a INSERT_STRING gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.INSERT_STRING), new InsertStringGestureData(kbmEvent.getCharTyped(), kbmTranslator.isKeyDown(Key.SHIFT)));
+			}
+		});
+		
+		// Add the MOUSE_MOVED -> MOVE_CURSOR gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "MOUSE_MOVED -> MOVE_CURSOR"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check if it's a mouse movement event
+				if (kbmEvent.getEventType() != KBMInputEvent.EventType.MOUSE_MOVE) return null;
+				
+				// Generate a CURSOR_MOVED gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.MOVE_CURSOR), new GestureData(kbmEvent.getMouseMove().getSecondEnd().clone()));
+			}
+		});
+		
+		// Add the LEFT CLICK + MOUSE_MOVE -> SELECT_AREA gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private static final int NO_CONSTRAINT = 0;
+			private SelectAreaGestureData _draggedFromData = null;
+			private int _constrainedDirection = NO_CONSTRAINT;
+			
+			@Override
+			public String details() { return "LEFT CLICK + MOUSE_MOVE -> SELECT_AREA"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check we are holding the left button only
+				if (!kbmTranslator.isOnlyMouseButtonDown(MouseButton.LEFT)) {
+					// If we were dragging, reset the dragging tracker and send the drag-finished gesture
+					if (_draggedFromData != null) {
+						SelectAreaGestureData finalData = _draggedFromData.finalise();
+						_draggedFromData = null;
+						return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.SELECT_AREA), finalData);
+						
+					// Otherwise just return
+					} else {
+						return null;
+					}
+				}
+				
+				// Check if it's a mouse movement event
+				if (kbmEvent.getEventType() != KBMInputEvent.EventType.MOUSE_MOVE) return null;
+				
+				// If we are just starting to drag, save the start position of the gesture
+				if (_draggedFromData == null) {
+					_draggedFromData = new SelectAreaGestureData(kbmEvent.getMouseMove().getSecondEnd().clone(), kbmEvent.getMouseMove().getFirstEnd().clone());
+				}
+				
+				// If holding control, move the mouse cursor to a constrained position
+				Point newMousePos = kbmEvent.getMouseMove().getFirstEnd().clone();
+/*				if (kbmTranslator.isKeyDown(Key.CTRL) && FreeItems.hasItemsAttachedToCursor()) {
+					if (_constrainedDirection == NO_CONSTRAINT) {
+						// Decide the direction of constraint
+						_constrainedDirection = Constraint.DIAGONAL_POS;
+						int absX = kbmEvent.getMouseMove().deltaX();
+						if (absX < 0) {
+							absX = -absX;
+							_constrainedDirection = Constraint.DIAGONAL_NEG;
+						}
+						int absY = kbmEvent.getMouseMove().deltaY();
+						if (absY < 0) {
+							absY = -absY;
+							if (_constrainedDirection == Constraint.DIAGONAL_NEG) {
+								_constrainedDirection = Constraint.DIAGONAL_POS;
+							} else {
+								_constrainedDirection = Constraint.DIAGONAL_NEG;
+							}
+						}
+						if (absY <= absX / 2) {
+							_constrainedDirection = Constraint.HORIZONTAL;
+						} else if (absX <= absY / 2) {
+							_constrainedDirection = Constraint.VERTICAL;
+						}
+					}
+					
+					int dX, dY;
+					if (_constrainedDirection == Constraint.HORIZONTAL) {
+						dX = kbmEvent.getMouseMove().deltaX();
+						dY = 0;
+					} else if (_constrainedDirection == Constraint.VERTICAL) {
+						dX = 0;
+						dY = kbmEvent.getMouseMove().deltaY();
+					} else if (_constrainedDirection == Constraint.DIAGONAL_POS) {
+						dX = dY = (kbmEvent.getMouseMove().deltaX() + kbmEvent.getMouseMove().deltaY()) / 2;
+					} else {
+						dY = (kbmEvent.getMouseMove().deltaY() - kbmEvent.getMouseMove().deltaX()) / 2;
+						dX = -dY;
+					}
+					
+					// Cause feedback loop by moving the mouse to the calculated position
+					newMousePos.add(1, 1);
+				} else*/ {
+					_constrainedDirection = NO_CONSTRAINT;
+					newMousePos = kbmEvent.getMouseMove().getSecondEnd().clone();
+				}
+
+				// Generate a SELECT_AREA gesture
+				_draggedFromData = _draggedFromData.carryOn(newMousePos);
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.SELECT_AREA), _draggedFromData);
+			}
+		});
+		
+		// Add the HOME/END/LEFT/RIGHT/PAGE_UP/PAGE_DOWN -> NAVIGATE_FRAME gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "HOME/END/LEFT/RIGHT/PAGE_UP/PAGE_DOWN/BACKSPACE -> NAVIGATE_FRAME"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Alt is for mouse emulation
+				if (kbmTranslator.isKeyDown(Key.ALT)) return null;
+				
+				// Decide which frame to navigate to
+				NavigateFrameGestureData.NavigateTo navigateTo;
+				if (kbmEvent.getKeyDown() == Key.HOME) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateFrameGestureData.NavigateTo.EARLIEST_FRAME;
+					} else {
+						navigateTo = NavigateFrameGestureData.NavigateTo.ZERO_FRAME;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.END) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateFrameGestureData.NavigateTo.LATEST_FRAME;
+					} else {
+						navigateTo = NavigateFrameGestureData.NavigateTo.LAST_FRAME;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.LEFT_ARROW && !kbmTranslator.isKeyDown(Key.CTRL)) {
+					navigateTo = NavigateFrameGestureData.NavigateTo.PREVIOUS_FRAME;
+				} else if(kbmEvent.getKeyDown() == Key.RIGHT_ARROW && !kbmTranslator.isKeyDown(Key.CTRL)) {
+					navigateTo = NavigateFrameGestureData.NavigateTo.NEXT_FRAME;
+				} else if(kbmEvent.getKeyDown() == Key.PAGE_UP) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateFrameGestureData.NavigateTo.BACK_FRAME;
+					} else {
+						navigateTo = NavigateFrameGestureData.NavigateTo.NEXT_FRAME;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.PAGE_DOWN) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateFrameGestureData.NavigateTo.FORWARD_FRAME;
+					} else {
+						navigateTo = NavigateFrameGestureData.NavigateTo.PREVIOUS_FRAME;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.BACKSPACE && kbmTranslator.isKeyDown(Key.CTRL)) {
+					navigateTo = NavigateFrameGestureData.NavigateTo.BACK_FRAME;
+				} else {
+					return null;
+				}
+				
+				// Check we aren't over text
+				Item on = FrameUtils.getCurrentItem();
+				if (on != null && on instanceof Text && !on.isFrameName()) return null;
+				
+				// Generate a CURSOR_MOVED gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.NAVIGATE_FRAME), new NavigateFrameGestureData(navigateTo));
+			}
+		});
+		
+		// Add the HOME/END/LEFT/RIGHT/PAGE_UP/PAGE_DOWN -> NAVIGATE_TEXT gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "HOME/END/LEFT/RIGHT/PAGE_UP/PAGE_DOWN -> NAVIGATE_TEXT"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Alt is for mouse emulation
+				if (kbmTranslator.isKeyDown(Key.ALT)) return null;
+				
+				// Decide which frame to navigate to
+				NavigateTextGestureData.NavigateTo navigateTo;
+				if (kbmEvent.getKeyDown() == Key.HOME) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateTextGestureData.NavigateTo.PARAGRAPH_HOME;
+					} else {
+						navigateTo = NavigateTextGestureData.NavigateTo.LINE_HOME;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.END) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateTextGestureData.NavigateTo.PARAGRAPH_END;
+					} else {
+						navigateTo = NavigateTextGestureData.NavigateTo.LINE_END;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.LEFT_ARROW) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateTextGestureData.NavigateTo.PREVIOUS_WORD;
+					} else {
+						navigateTo = NavigateTextGestureData.NavigateTo.PREVIOUS_CHARACTER;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.RIGHT_ARROW) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateTextGestureData.NavigateTo.NEXT_WORD;
+					} else {
+						navigateTo = NavigateTextGestureData.NavigateTo.NEXT_CHARACTER;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.UP_ARROW) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateTextGestureData.NavigateTo.PREVIOUS_TEXT_ITEM;
+					} else {
+						navigateTo = NavigateTextGestureData.NavigateTo.ABOVE_LINE;
+					}
+				} else if(kbmEvent.getKeyDown() == Key.DOWN_ARROW) {
+					if (kbmTranslator.isKeyDown(Key.CTRL)) {
+						navigateTo = NavigateTextGestureData.NavigateTo.NEXT_TEXT_ITEM;
+					} else {
+						navigateTo = NavigateTextGestureData.NavigateTo.BELOW_LINE;
+					}
+				} else {
+					return null;
+				}
+				
+				// Check we are over text
+				Item on = FrameUtils.getCurrentItem();
+				if (on == null || !(on instanceof Text) || on.isFrameName()) return null;
+				
+				// See if we are holding shift to select text
+				boolean selecting = kbmTranslator.isKeyDown(Key.SHIFT); 
+				
+				// Generate a CURSOR_MOVED gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.NAVIGATE_TEXT), new NavigateTextGestureData(navigateTo, selecting));
+			}
+		});
+		
+		// Add the SHIFT + LEFT_CLICK + MOUSE_MOVE -> PAN gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private Point _panStart;
+
+			@Override
+			public String details() { return "SHIFT + LEFT_CLICK + MOUSE_MOVE -> PAN"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// See if we are panning
+				if (ExperimentalFeatures.MousePan.get() &&
+						!FrameUtils.hasCurrentItem() &&
+						kbmTranslator.isMouseButtonDown(MouseButton.LEFT) &&
+						kbmTranslator.isKeyDown(Key.SHIFT) &&
+						kbmEvent.getEventType() == KBMInputEvent.EventType.MOUSE_MOVE)
+				{
+					if (_panStart == null) {
+						_panStart = kbmEvent.getMouseMove().getFirstEnd().clone();
+					}
+				} else {
+					_panStart = null;
+					return null;
+				}
+				
+				// Generate a PAN gesture
+				Point panPosition = kbmEvent.getMouseMove().getSecondEnd();
+				Point panDelta = Point.difference(kbmEvent.getMouseMove().getSecondEnd(), kbmEvent.getMouseMove().getFirstEnd());
+				
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PAN), new PanGestureData(panPosition, _panStart, panDelta));
+			}
+		});
+		
+		// Add the ALT + ARROW_KEYS -> MOVE_CURSOR gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private final int MACRO_CURSOR_MOVE_DISTANCE = 20;
+			private final int MICRO_CURSOR_MOVE_DISTANCE = 1;
+			
+			@Override
+			public String details() { return "ALT + ARROW_KEYS -> MOVE_CURSOR"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Can return if not pressing any of the arrow keys
+				if (	(kbmEvent.getKeyDown() != Key.UP_ARROW) &&
+						(kbmEvent.getKeyDown() != Key.DOWN_ARROW) &&
+						(kbmEvent.getKeyDown() != Key.LEFT_ARROW) &&
+						(kbmEvent.getKeyDown() != Key.RIGHT_ARROW))
+				{
+					return null;
+				}
+				
+				// Must be holding ALT for mouse emulation
+				if (!kbmTranslator.isKeyDown(Key.ALT)) return null;
+
+				// Decide how far to move
+				int distance = kbmTranslator.isKeyDown(Key.SHIFT) ? MICRO_CURSOR_MOVE_DISTANCE : MACRO_CURSOR_MOVE_DISTANCE;
+				
+				// Calculate the new cursor position
+				Point cursorPosition = EcosystemManager.getInputManager().getCursorPosition();
+				if (kbmEvent.getKeyDown() == Key.UP_ARROW) cursorPosition.y -= distance;
+				if (kbmEvent.getKeyDown() == Key.DOWN_ARROW) cursorPosition.y += distance;
+				if (kbmEvent.getKeyDown() == Key.LEFT_ARROW) cursorPosition.x -= distance;
+				if (kbmEvent.getKeyDown() == Key.RIGHT_ARROW) cursorPosition.x += distance;
+				
+				// Create the MOVE_CURSOR gesture and return it
+				Gesture moveCursorGesture = new Gesture(StandardGestureActions.getInstance().gestureType(StandardGestureType.MOVE_CURSOR), new SelectAreaGestureData(cursorPosition));
+				moveCursorGesture.setRobotic(true);
+				return Gesture.single(moveCursorGesture);
+			}
+		});
+		
+		// Add the F1/F2 -> ZOOM_IN/OUT gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private final float ZOOM_IN_SCALE_FACTOR = 1.1f;
+			private final float ZOOM_OUT_SCALE_FACTOR = 1f / ZOOM_IN_SCALE_FACTOR;
+			
+			@Override
+			public String details() { return "F1/F2 -> ZOOM_IN/OUT"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Decide on a scale factor
+				float scaleFactor;
+				if (kbmEvent.getKeyDown() == KBMInputEvent.Key.F1) {
+					scaleFactor = ZOOM_IN_SCALE_FACTOR;
+				} else if (kbmEvent.getKeyDown() == KBMInputEvent.Key.F2) {
+					scaleFactor = ZOOM_OUT_SCALE_FACTOR;
+				} else {
+					return null;
+				}
+				
+				// Return if zoom is disabled
+				if (!ExperimentalFeatures.FrameZoom.get()) return null;
+				
+				// Make sure we are pointing at free space (or the frame name)
+				Item currentItem = FrameUtils.getCurrentItem();
+				Collection<Item> currentItems = FrameUtils.getCurrentItems(currentItem);
+				if ((currentItem != null && !currentItem.isFrameName()) || (currentItems != null && currentItems.size() > 0)) return null;
+				
+				// Decide on where to zoom around
+				Point centreOfZoom;
+				if (ExperimentalFeatures.FrameZoomAroundCursor.get()) {
+					centreOfZoom = EcosystemManager.getInputManager().getCursorPosition();
+				} else {
+					centreOfZoom = new Point(0, 0);
+				}
+				
+				// Return zoom gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.ZOOM), new ZoomGestureData(centreOfZoom, scaleFactor));
+			}
+		});
+		
+		// Add the F1/F2 -> SCALE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private final int SCALE_UP_FACTOR = 1;
+			private final int SCALE_DOWN_FACTOR = -1;
+			
+			@Override
+			public String details() { return "F1/F2 -> SCALE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Decide whether to scale up or down or not at all
+				int scaleFactor;
+				if (kbmEvent.getKeyDown() == KBMInputEvent.Key.F1) {
+					scaleFactor = SCALE_UP_FACTOR;
+				} else if (kbmEvent.getKeyDown() == KBMInputEvent.Key.F2) {
+					scaleFactor = SCALE_DOWN_FACTOR;
+				} else {
+					return null;
+				}
+				
+				// Decide where to scale around
+				ScaleGestureData data;
+				if (kbmTranslator.isKeyDown(Key.CTRL)) {
+					data = new ScaleGestureData(scaleFactor);
+				} else {
+					data = new ScaleGestureData(null, scaleFactor);
+				}
+				
+				// Return scale gestures
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.SCALE), data);
+			}
+		});
+		
+		// Add the F3 -> CHANGE_COLOUR gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F3 -> CHANGE_COLOUR"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F3 to change colours
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F3) return null;
+				
+				// Decide how to change the colour
+				boolean shouldSetTransparent = kbmTranslator.isKeyDown(Key.SHIFT);
+				boolean shouldSetSecondaryColour = kbmTranslator.isKeyDown(Key.CTRL);
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.CHANGE_COLOUR), new ChangeColourGestureData(shouldSetTransparent, shouldSetSecondaryColour));
+			}
+		});
+		
+		// Add the F4 -> TOGGLE_ANNOTATION gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F4 -> TOGGLE_ANNOTATION"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F4 to toggle annotations
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F4) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.TOGGLE_ANNOTATION), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the F5 -> INSERT_DATE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F5 -> INSERT_DATE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F5 to insert the date
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F5) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.INSERT_DATE), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the F6 -> NEW_FRAMESET gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F6 -> NEW_FRAMESET"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F6 to create a new frameset
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F6) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.NEW_FRAMESET), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the F7 -> ROTATE_FONT_STYLE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F7 -> ROTATE_FONT_STYLE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F7 to rotate font style
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F7) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.ROTATE_FONT_STYLE), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the F8 -> ROTATE_FONT_FAMILY gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F8 -> ROTATE_FONT_FAMILY"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F8 to rotate font family
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F8) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.ROTATE_FONT_FAMILY), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the F9 -> TOGGLE_AUDIENCE_MODE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F9 -> TOGGLE_AUDIENCE_MODE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F9 to toggle audience mode
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F9) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.ROTATE_DISPLAY_MODE), null);
+			}
+		});
+		
+		// Add the F10 -> TOGGLE_XRAY_MODE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F10 -> TOGGLE_XRAY_MODE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F10 to xray mode
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F10) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.TOGGLE_XRAY_MODE), null);
+			}
+		});
+		
+		// Add the F11 -> SAVE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F11 -> SAVE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F11 to save
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F11) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.SAVE), null);
+			}
+		});
+		
+		// Add the F12 -> REFRESH gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "F12 -> REFRESH"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+
+				// Must be pressing F12 to refresh
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F12) return null;
+				
+				// Decide how to refresh
+				boolean shouldReloadFrameFirst = kbmTranslator.isKeyDown(Key.CTRL);
+				boolean shouldRefreshFrameSize = kbmTranslator.isKeyDown(Key.SHIFT);
+				
+				// Return the gesture
+				return standardSingle(StandardGestureType.REFRESH, new RefreshGestureData(shouldReloadFrameFirst, shouldRefreshFrameSize));
+			}
+		});
+		
+		// Add the ESC -> DROP_DOWN gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "ESC -> DROP_DOWN"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Shift/Ctrl + Esc is calculate
+				if (kbmTranslator.isKeyDown(Key.SHIFT) || kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing ESC to drop down
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.ESC) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.DROP_DOWN), null);
+			}
+		});
+		
+		// Add the SHIFT/CTRL + ESC -> CALCULATE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "SHIFT/CTRL + ESC -> CALCULATE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that shift or ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.SHIFT) && !kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing Esc to calculate
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.ESC) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.CALCULATE), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + TAB -> CALCULATE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + TAB -> CALCULATE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing tab to calculate
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.TAB) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.CALCULATE), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + L -> LINK gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + L -> LINK"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing L to link
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.L) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.LINK), new LinkGestureData(false));
+			}
+		});
+		
+		// Add the CTRL + G -> LINK gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + G -> LINK"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing G to link
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.G) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.LINK), new LinkGestureData(true));
+			}
+		});
+		
+		// Add the CTRL + A -> ACTION gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + A -> ACTION"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing A to action
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.A) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.ACTION), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + B -> TOGGLE_BOLD gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + B -> TOGGLE_BOLD"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing B to toggle bold
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.B) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.TOGGLE_BOLD), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + I -> TOGGLE_ITALICS gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + I -> TOGGLE_ITALICS"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing I to toggle italics
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.I) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.TOGGLE_ITALICS), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + V -> PASTE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + V -> PASTE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing V to paste
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.V) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PASTE), null);
+			}
+		});
+		
+		// Add the CTRL + C -> COPY gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + C -> COPY"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing C to copy
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.C) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.COPY), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + C -> MAKE_CIRCLE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + C -> MAKE_CIRCLE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing C to make circle
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.C) return null;
+				
+				// Make sure we aren't trying to do a copy-clone 
+				if (FreeItems.hasItemsAttachedToCursor()) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.MAKE_CIRCLE), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + X -> CUT gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + X -> CUT"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing X to cut
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.X) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.CUT), null);
+			}
+		});
+		
+		// Add the CTRL + M -> TOGGLE_ITEMS_MARK gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + M -> TOGGLE_ITEMS_MARK"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing M to toggle items mark
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.M) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.TOGGLE_ITEMS_MARK), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the CTRL + Z -> UNDO gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + Z -> UNDO"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing Z to undo
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.X) return null;
+				
+				// Holding shift as well makes this a redo gesture
+				boolean redo = kbmTranslator.isKeyDown(Key.SHIFT);
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.UNDO), new UndoGestureData(redo));
+			}
+		});
+		
+		// Add the CTRL + D -> INSERT_STRING gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + D -> INSERT_STRING"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing D to delete character
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.D) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.INSERT_STRING), new InsertStringGestureData(Text.DELETE_CHARACTER, false));
+			}
+		});
+		
+		// Add the CTRL + DELETE -> DELETE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + DELETE -> DELETE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing DELETE to delete
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.DELETE) return null;
+				
+				// Decide if this is an alternate mode delete
+				// TODO: What specifically is alternate mode delete? cts16
+				boolean alternateMode = kbmTranslator.isKeyDown(Key.SHIFT);
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.DELETE), new DeleteGestureData(alternateMode));
+			}
+		});
+		
+		// Add the CTRL + F -> FORMAT gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + F -> FORMAT"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing F to format
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.F) return null;
+				
+				// Decide if this is an horizontal or vertical format
+				boolean horizontal = kbmTranslator.isKeyDown(Key.SHIFT);
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.FORMAT), new FormatGestureData(horizontal));
+			}
+		});
+		
+		// Add the CTRL + J -> JUSTIFY gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + J -> JUSTIFY"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing J to justify
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.J) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.JUSTIFY), new JustifyGestureData(false));
+			}
+		});
+		
+		// Add the CTRL + R -> JUSTIFY gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + R -> JUSTIFY"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Must be pressing R to justify
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.R) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.JUSTIFY), new JustifyGestureData(true));
+			}
+		});
+		
+		// Add the CTRL + S -> SAVE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + S -> SAVE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Ctrl + Shift + S is split text
+				if (kbmTranslator.isKeyDown(Key.SHIFT)) return null;
+				
+				// Must be pressing S to save
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.S) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.SAVE), null);
+			}
+		});
+		
+		// Add the CTRL + SHIFT + S -> SPLIT_TEXT gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "CTRL + SHIFT + S -> SPLIT_TEXT"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check that ctrl is down
+				if (!kbmTranslator.isKeyDown(Key.CTRL)) return null;
+				
+				// Ctrl + Shift + S is split text
+				if (!kbmTranslator.isKeyDown(Key.SHIFT)) return null;
+				
+				// Must be pressing S to save
+				if (kbmEvent.getKeyDown() != KBMInputEvent.Key.S) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.SPLIT_TEXT), new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the MIDDLE MOUSE + MOUSE_MOVE -> PICK_UP gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private SelectAreaGestureData _draggedFromData = null;
+			
+			@Override
+			public String details() { return "MIDDLE MOUSE + MOUSE_MOVE -> PICK_UP"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check we are holding the middle button only
+				if (!kbmTranslator.isOnlyMouseButtonDown(MouseButton.MIDDLE)) {
+					// If we were dragging, reset the dragging tracker and send the drag-finished gesture
+					if (_draggedFromData != null) {
+						SelectAreaGestureData finalData = _draggedFromData.finalise();
+						_draggedFromData = null;
+						return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PICK_UP), finalData);
+						
+					// Otherwise just return
+					} else {
+						return null;
+					}
+				}
+				
+				// Check if it's a mouse movement event
+				if (kbmEvent.getEventType() != KBMInputEvent.EventType.MOUSE_MOVE) return null;
+				
+				// See if we should inherit the attributes of the thing we're picking up
+				boolean inheritAttributes = kbmTranslator.isKeyDown(Key.SHIFT);
+				
+				// If we are just starting to drag, save the start position of the gesture
+				if (_draggedFromData == null) {
+					_draggedFromData = new PickUpGestureData(kbmEvent.getMouseMove().getSecondEnd().clone(), kbmEvent.getMouseMove().getFirstEnd().clone(), false, inheritAttributes);
+				}
+				
+				// Wait till we have moved out of no-op range before performing drag-pickup
+				if (!isMouseDragNoOp()) return null;
+				
+				// Generate a PICK_UP gesture
+				_draggedFromData = _draggedFromData.carryOn(kbmEvent.getMouseMove().getSecondEnd().clone());
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PICK_UP), _draggedFromData);
+			}
+		});
+				
+		// Add the MIDDLE MOUSE -> PICK_UP gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private Boolean _pickup = null;
+			private Item _clickDownOn = null;
+			
+			@Override
+			public String details() { return "MIDDLE MOUSE -> PICK_UP"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check for single middle click
+				if (_pickup == null && kbmEvent.getMouseButtonDown() == MouseButton.MIDDLE) {
+					_pickup = true;
+					_clickDownOn = FrameUtils.getCurrentItem();
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_pickup = false;
+					return null;
+				} else if (Boolean.FALSE.equals(_pickup) && kbmTranslator.isAnyMouseButtonDown() == false) {
+					_pickup = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the pickup
+				if (!Boolean.TRUE.equals(_pickup) || kbmEvent.getMouseButtonUp() != MouseButton.MIDDLE) return null;
+				
+				// Reset pickup for next gesture
+				_pickup = null;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					if (!(_clickDownOn instanceof Text)) printNoopMessage();
+					return null;
+				}
+
+				// Check we should be picking up (shift forces line creation)
+				if (FreeItems.hasItemsAttachedToCursor() || kbmTranslator.isKeyDown(Key.SHIFT)) return null;
+				
+				// Check there is something to pick up
+				if (!FrameUtils.hasCurrentItem() && FrameUtils.getCurrentItems() == null) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PICK_UP), new PickUpGestureData(false));
+			}
+		});
+		
+		// Add the RIGHT MOUSE + MOUSE_MOVE -> PICK_UP gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private SelectAreaGestureData _draggedFromData = null;
+			
+			@Override
+			public String details() { return "RIGHT MOUSE + MOUSE_MOVE -> PICK_UP"; }
+			
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check we are holding the middle button only
+				if (!kbmTranslator.isOnlyMouseButtonDown(MouseButton.RIGHT)) {
+					// If we were dragging, reset the dragging tracker and send the drag-finished gesture
+					if (_draggedFromData != null) {
+						SelectAreaGestureData finalData = _draggedFromData.finalise();
+						_draggedFromData = null;
+						return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PICK_UP), finalData);
+						
+					// Otherwise just return
+					} else {
+						return null;
+					}
+				}
+				
+				// Check if it's a mouse movement event
+				if (kbmEvent.getEventType() != KBMInputEvent.EventType.MOUSE_MOVE) return null;
+				
+				// See if we should inherit the attributes of the thing we're picking up
+				boolean inheritAttributes = kbmTranslator.isKeyDown(Key.SHIFT);
+				
+				// If we are just starting to drag, save the start position of the gesture
+				if (_draggedFromData == null) {
+					_draggedFromData = new PickUpGestureData(kbmEvent.getMouseMove().getSecondEnd().clone(), kbmEvent.getMouseMove().getFirstEnd().clone(), true, inheritAttributes);
+				}
+				
+				// Wait till we have moved out of no-op range before performing drag-pickup
+				if (!isMouseDragNoOp()) return null;
+				
+				// Generate a PICK_UP gesture
+				_draggedFromData = _draggedFromData.carryOn(kbmEvent.getMouseMove().getSecondEnd().clone());
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PICK_UP), _draggedFromData);
+			}
+		});
+		
+		// Add the RIGHT MOUSE -> PICK_UP gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(new InputType[] { InputType.KBM, InputType.TIMEOUT })
+		{
+			private final static int EXTRUSION_DELAY_MS = 750;
+			private final static int PULSE_AMOUNT = 2;
+			private Boolean _pickup = null;
+			private Item _clickedItem = null;
+			private TimeoutHandle _timeoutHandle;
+			private boolean _extrusionDelayExpired = false;
+			
+			@Override
+			public String details() { return "RIGHT MOUSE -> PICK_UP"; }
+
+			public List<Gesture> onInputEvent(InputEvent event)
+			{
+				if (event == null) {
+					return null;
+				} else if (event.getInputType() == InputType.KBM) {
+					return onKBMInputEvent((KBMInputEvent) event);
+				} else if (event.getInputType() == InputType.TIMEOUT) {
+					return onTimeoutInputEvent((TimeoutInputEvent) event);
+				}
+				
+				return null;
+			}
+			
+			private List<Gesture> onKBMInputEvent(KBMInputEvent kbmEvent)
+			{
+				// Check for single right click
+				if (_pickup == null && kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+					_pickup = true;
+					_timeoutHandle = EcosystemManager.getInputManager().addTimeout(EXTRUSION_DELAY_MS);
+					_extrusionDelayExpired = false;
+					_clickedItem = FrameUtils.getCurrentItem();
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_pickup = false;
+					EcosystemManager.getInputManager().cancelTimeout(_timeoutHandle);
+					return null;
+				} else if (Boolean.FALSE.equals(_pickup) && kbmTranslator.isAnyMouseButtonDown() == false) {
+					_pickup = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the pickup
+				if (!Boolean.TRUE.equals(_pickup) || kbmEvent.getMouseButtonUp() != MouseButton.RIGHT) return null;
+				
+				// Reset pickup for next gesture
+				_pickup = null;
+				
+				// Cancel the extrusion timeout if it's pending
+				EcosystemManager.getInputManager().cancelTimeout(_timeoutHandle);
+				
+				// Reset the line pulse if it occured
+				if (_extrusionDelayExpired) pulseExtrusionLines(false);
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+				
+				// Check we should be picking up (shift forces line creation)
+				if (FreeItems.hasItemsAttachedToCursor() || kbmTranslator.isKeyDown(Key.SHIFT)) return null;
+				
+				// Check there is something to pick up
+				if (!FrameUtils.hasCurrentItem() && FrameUtils.getCurrentItems() == null) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PICK_UP), new PickUpGestureData(true, _extrusionDelayExpired));
+			}
+			
+			private List<Gesture> onTimeoutInputEvent(TimeoutInputEvent event)
+			{
+				if (event.getHandle() == _timeoutHandle) {
+					_extrusionDelayExpired = true;
+					pulseExtrusionLines(true);
+				}
+				
+				return null;
+			}
+			
+			private void pulseExtrusionLines(boolean pulseOn)
+			{
+				int amount = PULSE_AMOUNT;
+				if (!pulseOn) amount *= -1;
+
+				if (_clickedItem != null) {
+					for (Item i : _clickedItem.getAllConnected()) {
+						if (i instanceof Line) {
+							Line line = (Line) i;
+							line.setThickness(line.getThickness() + amount);
+						}
+					}
+				}
+				
+				DisplayController.requestRefresh(true);
+			}
+		});
+		
+		// Add the MIDDLE MOUSE -> PLACE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			Boolean _place = null;
+			
+			@Override
+			public String details() { return "MIDDLE MOUSE -> PLACE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check for single middle click
+				if (_place == null && kbmEvent.getMouseButtonDown() == MouseButton.MIDDLE) {
+					_place = true;
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_place = false;
+					return null;
+				} else if (Boolean.FALSE.equals(_place) && isAnyMouseButtonDown() == false) {
+					_place = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the place
+				if (!Boolean.TRUE.equals(_place) || kbmEvent.getMouseButtonUp() != MouseButton.MIDDLE) return null;
+				
+				// Reset place for next gesture
+				_place = null;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+				
+				// Check we should be placing
+				if (!FreeItems.hasItemsAttachedToCursor()) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PLACE), new PickUpGestureData(false));
+			}
+		});
+		
+		// Add the RIGHT MOUSE -> PLACE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			Boolean _place = null;
+			@Override
+			public String details() { return "RIGHT MOUSE -> PLACE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check for single right click
+				if (_place == null && kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+					_place = true;
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_place = false;
+					return null;
+				} else if (Boolean.FALSE.equals(_place) && kbmTranslator.isAnyMouseButtonDown() == false) {
+					_place = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the place
+				if (!Boolean.TRUE.equals(_place) || kbmEvent.getMouseButtonUp() != MouseButton.RIGHT) return null;
+				
+				// Reset place for next gesture
+				_place = null;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+				
+				// Check we should be placing
+				if (!FreeItems.hasItemsAttachedToCursor()) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.PLACE), new PickUpGestureData(true));
+			}
+		});
+		
+		// Add the MIDDLE + RIGHT MOUSE -> DELETE gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private static final int STATE_NO_MOUSE_BUTTON_DOWN = 0;
+			private static final int STATE_MIDDLE_MOUSE_BUTTON_DOWN = 1;
+			private static final int STATE_RIGHT_MOUSE_BUTTON_DOWN = 2;
+			private static final int STATE_BOTH_MOUSE_BUTTON_DOWN = 3;
+			private static final int STATE_ERROR = 4;
+			private int _state = STATE_NO_MOUSE_BUTTON_DOWN;
+			
+			@Override
+			public String details() { return "MIDDLE + RIGHT MOUSE -> DELETE"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Keep track of mouse button state
+				if (_state == STATE_NO_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+						_state = STATE_RIGHT_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonDown() == MouseButton.MIDDLE) {
+						_state = STATE_MIDDLE_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_MIDDLE_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+						_state = STATE_BOTH_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonUp() == MouseButton.MIDDLE) {
+						_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_RIGHT_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.MIDDLE) {
+						_state = STATE_BOTH_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonUp() == MouseButton.RIGHT) {
+						_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_BOTH_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+						return null;
+					}
+				} else if (kbmTranslator.isAnyMouseButtonDown() == false) {
+					_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					return null;
+				}
+				
+				// Wait for the button up to perform the delete
+				if (_state != STATE_BOTH_MOUSE_BUTTON_DOWN || kbmTranslator.isAnyMouseButtonDown()) return null;
+				
+				// Reset state for next gesture
+				_state = STATE_NO_MOUSE_BUTTON_DOWN;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+				
+				// Decide if this is an alternate mode delete
+				// TODO: What specifically is alternate mode delete? cts16
+				boolean alternateMode = kbmTranslator.isKeyDown(Key.SHIFT);
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.DELETE), new DeleteGestureData(alternateMode));
+			}
+		});
+		
+		// Add the MIDDLE MOUSE -> CREATE_ITEM gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			Boolean _create = null;
+			@Override
+			public String details() { return "MIDDLE MOUSE -> CREATE_ITEM"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check for single middle click
+				if (_create == null && kbmEvent.getMouseButtonDown() == MouseButton.MIDDLE) {
+					_create = true;
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_create = false;
+					return null;
+				} else if (Boolean.FALSE.equals(_create) && kbmTranslator.isAnyMouseButtonDown() == false) {
+					_create = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the create
+				if (!Boolean.TRUE.equals(_create) || kbmEvent.getMouseButtonUp() != MouseButton.MIDDLE) return null;
+				
+				// Reset create for next gesture
+				_create = null;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+				
+				// Check we can create
+				if (FreeItems.hasItemsAttachedToCursor()) return null;
+				
+				// Check we're not trying to pick up (shift forces item creation)
+				if (!kbmTranslator.isKeyDown(Key.SHIFT) && (FrameUtils.hasCurrentItem() || FrameUtils.getCurrentItems() != null)) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.CREATE_ITEM), new CreateItemGestureData(ItemType.LINE));
+			}
+		});
+		
+		// Add the RIGHT MOUSE -> CREATE_ITEM gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			Boolean _create = null;
+			@Override
+			public String details() { return "RIGHT MOUSE -> CREATE_ITEM"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check for single middle click
+				if (_create == null && kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+					_create = true;
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_create = false;
+					return null;
+				} else if (Boolean.FALSE.equals(_create) && kbmTranslator.isAnyMouseButtonDown() == false) {
+					_create = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the create
+				if (!Boolean.TRUE.equals(_create) || kbmEvent.getMouseButtonUp() != MouseButton.RIGHT) return null;
+				
+				// Reset create for next gesture
+				_create = null;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+				
+				// Check we can create
+				if (FreeItems.hasItemsAttachedToCursor()) return null;
+				
+				// Check we're not trying to pick up (shift forces item creation)
+				if (!kbmTranslator.isKeyDown(Key.SHIFT) && (FrameUtils.hasCurrentItem() || FrameUtils.getCurrentItems() != null)) return null;
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.CREATE_ITEM), new CreateItemGestureData(ItemType.BOX));
+			}
+		});
+		
+		// Add the LEFT MOUSE -> CLICK gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private static final int LONG_CLICK_DELAY_MS = 500;
+			private Boolean _click = null;
+			private long _clickStartTime;
+			@Override
+			public String details() { return "LEFT MOUSE -> CLICK"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Check for single left click
+				if (_click == null && kbmEvent.getMouseButtonDown() == MouseButton.LEFT) {
+					_click = true;
+					_clickStartTime = System.currentTimeMillis();
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_click = false;
+					return null;
+				} else if (Boolean.FALSE.equals(_click) && kbmTranslator.isAnyMouseButtonDown() == false) {
+					_click = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the click
+				if (!Boolean.TRUE.equals(_click) || kbmEvent.getMouseButtonUp() != MouseButton.LEFT) return null;
+				
+				// Reset click for next gesture
+				_click = null;
+				
+				// Check if this is a long click
+				boolean longClick = System.currentTimeMillis() >= _clickStartTime + LONG_CLICK_DELAY_MS;
+				
+				// Check if we are toggling arrowhead
+				if (longClick && FrameUtils.getCurrentItem() == null) {
+					if (FreeItems.getInstance().size() <= 2) {
+						for (Item i : FreeItems.getInstance()) {
+							if (i instanceof Line) {
+								return null;
+							}
+						}
+					}
+				}
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+				
+				// Return the gesture
+				return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.CLICK), new ClickGestureData(longClick));
+			}
+		});
+		
+		// Add the LEFT MOUSE -> TOGGLE_ARROWHEAD gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(new InputType[] { InputType.KBM, InputType.TIMEOUT })
+		{
+			private static final int LONG_CLICK_DELAY_MS = 500;
+			private Boolean _click = null;
+			private TimeoutHandle _timeoutHandle = null;
+			
+			@Override
+			public String details() { return "LEFT MOUSE -> TOGGLE_ARROWHEAD"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event)
+			{
+				if (event == null) {
+					return null;
+				} else if (event.getInputType() == InputType.KBM) {
+					return onKBMInputEvent((KBMInputEvent) event);
+				} else if (event.getInputType() == InputType.TIMEOUT) {
+					return onTimeoutInputEvent((TimeoutInputEvent) event);
+				}
+				
+				return null;
+			}
+			
+			private List<Gesture> onKBMInputEvent(KBMInputEvent kbmEvent)
+			{
+				// Check for single left click
+				if (_click == null && kbmEvent.getMouseButtonDown() == MouseButton.LEFT) {
+					_click = true;
+					_timeoutHandle = EcosystemManager.getInputManager().addTimeout(LONG_CLICK_DELAY_MS);
+					return null;
+				} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+					_click = false;
+					EcosystemManager.getInputManager().cancelTimeout(_timeoutHandle);
+					return null;
+				} else if (Boolean.FALSE.equals(_click) && kbmTranslator.isAnyMouseButtonDown() == false) {
+					_click = null;
+					return null;
+				}
+				
+				// Wait for the button up to perform the click
+				if (!Boolean.TRUE.equals(_click) || kbmEvent.getMouseButtonUp() != MouseButton.LEFT) return null;
+				
+				// Reset click for next gesture
+				_click = null;
+				
+				// Cancel the timer if it's still pending
+				EcosystemManager.getInputManager().cancelTimeout(_timeoutHandle);
+				
+				return null;
+			}
+			
+			private List<Gesture> onTimeoutInputEvent(TimeoutInputEvent event)
+			{
+				if (event.getHandle() == _timeoutHandle) {
+					// Check if we are toggling arrowhead
+					Item line = null;
+					if (FrameUtils.getCurrentItem() == null) {
+						if (FreeItems.getInstance().size() <= 2) {
+							for (Item i : FreeItems.getInstance()) {
+								if (i instanceof Line) {
+									line = i;
+								}
+							}
+						}
+					}
+					if (line == null) return null;
+					
+					// Check we haven't moved too far
+					if (isMouseDragNoOp()) {
+						printNoopMessage();
+						return null;
+					}
+					
+					return Gesture.single(StandardGestureActions.getInstance().gestureType(StandardGestureType.TOGGLE_ARROWHEAD), new ItemSpecificGestureData(line));
+				}
+				
+				// Someone else's timeout event
+				return null;
+			}
+		});
+		
+		// Add the LEFT + RIGHT MOUSE -> EXTRACT_ATTRIBUTES gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private static final int STATE_NO_MOUSE_BUTTON_DOWN = 0;
+			private static final int STATE_LEFT_MOUSE_BUTTON_DOWN = 1;
+			private static final int STATE_RIGHT_MOUSE_BUTTON_DOWN = 2;
+			private static final int STATE_BOTH_MOUSE_BUTTON_DOWN = 3;
+			private static final int STATE_ERROR = 4;
+			private int _state = STATE_NO_MOUSE_BUTTON_DOWN;
+			
+			@Override
+			public String details() { return "LEFT + RIGHT MOUSE -> EXTRACT_ATTRIBUTES"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Keep track of mouse button state
+				if (_state == STATE_NO_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+						_state = STATE_RIGHT_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonDown() == MouseButton.LEFT) {
+						_state = STATE_LEFT_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_LEFT_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+						_state = STATE_BOTH_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonUp() == MouseButton.LEFT) {
+						_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_RIGHT_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.LEFT) {
+						_state = STATE_BOTH_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonUp() == MouseButton.RIGHT) {
+						_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_BOTH_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+						return null;
+					}
+				} else if (kbmTranslator.isAnyMouseButtonDown() == false) {
+					_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					return null;
+				}
+				
+				// Wait for the button up to perform the extraction
+				if (_state != STATE_BOTH_MOUSE_BUTTON_DOWN || kbmTranslator.isAnyMouseButtonDown()) return null;
+				
+				// Reset state for next gesture
+				_state = STATE_NO_MOUSE_BUTTON_DOWN;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+
+				// Make sure we are over something to extract from
+				if (!FrameUtils.hasCurrentItem()) return null;
+				
+				// Return the gesture
+				return standardSingle(StandardGestureType.EXTRACT_ATTRIBUTES, new ItemSpecificGestureData());
+			}
+		});
+		
+		// Add the LEFT + RIGHT MOUSE -> FORMAT gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			private static final int STATE_NO_MOUSE_BUTTON_DOWN = 0;
+			private static final int STATE_LEFT_MOUSE_BUTTON_DOWN = 1;
+			private static final int STATE_RIGHT_MOUSE_BUTTON_DOWN = 2;
+			private static final int STATE_BOTH_MOUSE_BUTTON_DOWN = 3;
+			private static final int STATE_ERROR = 4;
+			private int _state = STATE_NO_MOUSE_BUTTON_DOWN;
+			
+			@Override
+			public String details() { return "LEFT + RIGHT MOUSE -> FORMAT"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// Keep track of mouse button state
+				if (_state == STATE_NO_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+						_state = STATE_RIGHT_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonDown() == MouseButton.LEFT) {
+						_state = STATE_LEFT_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_LEFT_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.RIGHT) {
+						_state = STATE_BOTH_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonUp() == MouseButton.LEFT) {
+						_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_RIGHT_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getMouseButtonDown() == MouseButton.LEFT) {
+						_state = STATE_BOTH_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getMouseButtonUp() == MouseButton.RIGHT) {
+						_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+					}
+					return null;
+				} else if (_state == STATE_BOTH_MOUSE_BUTTON_DOWN) {
+					if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+						_state = STATE_ERROR;
+						return null;
+					}
+				} else if (kbmTranslator.isAnyMouseButtonDown() == false) {
+					_state = STATE_NO_MOUSE_BUTTON_DOWN;
+					return null;
+				}
+				
+				// Wait for the button up to perform the extraction
+				if (_state != STATE_BOTH_MOUSE_BUTTON_DOWN || kbmTranslator.isAnyMouseButtonDown()) return null;
+				
+				// Reset state for next gesture
+				_state = STATE_NO_MOUSE_BUTTON_DOWN;
+				
+				// Check we haven't moved too far
+				if (isMouseDragNoOp()) {
+					printNoopMessage();
+					return null;
+				}
+
+				// Make sure we are not extracting
+				if (FrameUtils.hasCurrentItem()) return null;
+				
+				// Return the gestures
+				Gesture hFormat = new Gesture(StandardGestureActions.getInstance().gestureType(StandardGestureType.FORMAT), new FormatGestureData(true));
+				Gesture vFormat = new Gesture(StandardGestureActions.getInstance().gestureType(StandardGestureType.FORMAT), new FormatGestureData(false));
+				if (kbmTranslator.isKeyDown(Key.CTRL) && kbmTranslator.isKeyDown(Key.SHIFT)) {
+					List<Gesture> gestures = Gesture.single(hFormat);
+					gestures.add(vFormat);
+					return gestures;
+				} else if (kbmTranslator.isKeyDown(Key.CTRL)) {
+					return Gesture.single(hFormat);
+				} else {
+					return Gesture.single(vFormat);
+				}
+			}
+		});
+		
+		// TEMPLATE
+		// Add the DETAILS gesture recogniser
+		addTranslator(new InputEventToGestureTranslator(InputType.KBM)
+		{
+			@Override
+			public String details() { return "DETAILS"; }
+
+			@Override
+			public List<Gesture> onInputEvent(InputEvent event) {
+				// Cast to keyboard/mouse event
+				KBMInputEvent kbmEvent = (KBMInputEvent) event;
+				
+				// TODO Auto-generated method stub
+				return null;
+			}
+		});
+	}
+	
+	public boolean isKeyDown(Key key)
+	{
+		return StandardInputEventListeners.kbmStateListener.isKeyDown(key);
+	}
+	
+	public boolean isMouseButtonDown(MouseButton button)
+	{
+		return StandardInputEventListeners.kbmStateListener.isMouseButtonDown(button);
+	}
+	
+	public boolean isOnlyMouseButtonDown(MouseButton button)
+	{
+		return StandardInputEventListeners.kbmStateListener.isOnlyMouseButtonDown(button);
+	}
+	
+	public boolean isAnyMouseButtonDown()
+	{
+		return StandardInputEventListeners.kbmStateListener.isAnyMouseButtonDown();
+	}
+	
+	public static boolean isMouseDragNoOp()
+	{
+		return _noop;
+	}
+	
+	public static void printNoopMessage()
+	{
+		MessageBay.displayMessage("Action cancelled, mouse moved more than " + UserSettings.NoOpThreshold.get() + " pixels.");
+	}
+	
+	public static void trackMouseDragNoop(KBMInputEvent event)
+	{
+		if (!StandardInputEventListeners.kbmStateListener.isAnyMouseButtonDown() && event.getEventType() != EventType.MOUSE_BUTTON_UP) {
+			_noop = false;
+			_mouseDragStart = null;
+		} else if (_mouseDragStart == null) {
+			org.expeditee.core.Line mouseMove = event.getMouseMove();
+			if (mouseMove != null) _mouseDragStart = mouseMove.getFirstEnd().clone();
+		} else if (_noop == false) {
+			org.expeditee.core.Line mouseMove = event.getMouseMove();
+			if (mouseMove != null) {
+				mouseMove.setFirstEnd(_mouseDragStart.clone());
+				if (mouseMove.length() > UserSettings.NoOpThreshold.get()) {
+					_noop = true;
+				}
+			}
+		}
+	}
+	
+	/** Convenience function to create a single standard gesture. */
+	public static List<Gesture> standardSingle(StandardGestureType type, GestureData data)
+	{
+		return Gesture.single(StandardGestureActions.getInstance().gestureType(type), data);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/Gesture.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/Gesture.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/Gesture.java	(revision 1097)
@@ -0,0 +1,145 @@
+package org.expeditee.gio.gesture;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.expeditee.gio.gesture.data.GestureData;
+
+/**
+ * Class representing the abstract notion of input to Expeditee. Classes can implement
+ * the <code>GestureListener</code> interface and register themselves with the <code>InputManager</code>
+ * to receive notification of gestures, which are translated from the raw input (mouse/keyboard/touch/etc.).
+ * 
+ * @author cts16
+ */
+public class Gesture {
+	
+	/**
+	 * Class representing a type of gesture. Identified by a string name. Each name corresponds
+	 * to a single GestureType object throughout the entire program, so type matching can be
+	 * performed with the == operator. The GestureType object also is initialised with a 
+	 * GestureData type, which is the type of data that should come with gestures
+	 * of that type.
+	 * 
+	 * @author cts16
+	 */
+	public static class GestureType
+	{
+		/** Static mapping retains reference to the single GestureType object per string. */
+		private static HashMap<String, GestureType> _registeredTypes = new HashMap<String, GestureType>();
+		
+		/** The name of this gesture type. */
+		private String _name;
+		/** The type of data that goes with gestures of this type. */
+		private Class<? extends GestureData> _dataClass;
+		
+		public static GestureType register(String name)
+		{
+			if (name == null || isRegistered(name)) return null;
+			
+			_registeredTypes.put(name, new GestureType(name, null));
+			
+			return get(name);
+		}
+		
+		public static boolean isRegistered(String name)
+		{
+			return (name != null) && (_registeredTypes.containsKey(name));
+		}
+		
+		public static GestureType get(String name)
+		{
+			if (name == null || !isRegistered(name)) return null;
+			
+			return _registeredTypes.get(name);
+		}
+		
+		private GestureType(String name, Class<? extends GestureData> dataClass)
+		{
+			_name = name;
+			_dataClass = dataClass;
+		}
+		
+		public String name()
+		{
+			return _name;
+		}
+		
+		public Class<? extends GestureData> dataClass()
+		{
+			return _dataClass;
+		}
+	}
+	
+	/** The type of gesture that was performed. */
+	private GestureType _type;
+	
+	/** The specifics of the gesture (differs by gesture type). */
+	private GestureData _data;
+	
+	/** Whether or not the gesture was created by the program instead of an actual input event. */
+	private boolean _isRobotic = false;
+	
+	public Gesture(GestureType type, GestureData data)
+	{
+		assert(type != null);
+		
+		_type = type;
+		_data = data;
+	}
+	
+	public Gesture(Gesture other)
+	{
+		this(other._type, other._data);
+		setRobotic(other._isRobotic);
+	}
+	
+	public GestureType getType()
+	{
+		return _type;
+	}
+	
+	public GestureData getData()
+	{
+		return _data;
+	}
+	
+	/**
+	 * @return
+	 * 		True if the gesture was generated by software, false if it came from
+	 * 		an actual input event.
+	 */
+	public boolean isRobotic()
+	{
+		return _isRobotic;
+	}
+	
+	public void setRobotic(boolean isRobotic)
+	{
+		_isRobotic = isRobotic;
+	}
+	
+	public Gesture clone()
+	{
+		return new Gesture(this);
+	}
+	
+	/**
+	 * Helper-method to create a list of gestures with a single gesture in it.
+	 */
+	public static List<Gesture> single(GestureType type, GestureData data)
+	{
+		return single(new Gesture(type, data));
+	}
+	
+	/**
+	 * Helper-method to create a list of gestures with a single gesture in it.
+	 */
+	public static List<Gesture> single(Gesture gesture)
+	{
+		List<Gesture> gestures = new LinkedList<Gesture>();
+		gestures.add(gesture);
+		return gestures;
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/GestureAction.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/GestureAction.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/GestureAction.java	(revision 1097)
@@ -0,0 +1,7 @@
+package org.expeditee.gio.gesture;
+
+public interface GestureAction {
+
+	public void exec(Gesture gesture);
+	
+}
Index: /trunk/src/org/expeditee/gio/gesture/GestureListener.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/GestureListener.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/GestureListener.java	(revision 1097)
@@ -0,0 +1,13 @@
+package org.expeditee.gio.gesture;
+
+public interface GestureListener {
+
+	/**
+	 * What to do when the InputManager recognises a gesture.
+	 * 
+	 * @param gesture
+	 * 		The gesture performed.
+	 */
+	public void onGesture(Gesture gesture);
+	
+}
Index: /trunk/src/org/expeditee/gio/gesture/StandardGestureActions.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/StandardGestureActions.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/StandardGestureActions.java	(revision 1097)
@@ -0,0 +1,4531 @@
+package org.expeditee.gio.gesture;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.expeditee.actions.Actions;
+import org.expeditee.actions.Misc;
+import org.expeditee.actions.Navigation;
+import org.expeditee.core.Colour;
+import org.expeditee.core.Point;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.ClipboardManager.ClipboardData;
+import org.expeditee.gio.gesture.Gesture.GestureType;
+import org.expeditee.gio.gesture.data.ChangeColourGestureData;
+import org.expeditee.gio.gesture.data.ClickGestureData;
+import org.expeditee.gio.gesture.data.CreateItemGestureData;
+import org.expeditee.gio.gesture.data.CreateItemGestureData.ItemType;
+import org.expeditee.gio.gesture.data.DeleteGestureData;
+import org.expeditee.gio.gesture.data.FormatGestureData;
+import org.expeditee.gio.gesture.data.InsertStringGestureData;
+import org.expeditee.gio.gesture.data.ItemSpecificGestureData;
+import org.expeditee.gio.gesture.data.JustifyGestureData;
+import org.expeditee.gio.gesture.data.LinkGestureData;
+import org.expeditee.gio.gesture.data.SelectAreaGestureData;
+import org.expeditee.gio.gesture.data.NavigateFrameGestureData;
+import org.expeditee.gio.gesture.data.NavigateTextGestureData;
+import org.expeditee.gio.gesture.data.PanGestureData;
+import org.expeditee.gio.gesture.data.PickUpGestureData;
+import org.expeditee.gio.gesture.data.RefreshGestureData;
+import org.expeditee.gio.gesture.data.ScaleGestureData;
+import org.expeditee.gio.gesture.data.UndoGestureData;
+import org.expeditee.gio.gesture.data.ZoomGestureData;
+import org.expeditee.gio.input.StandardInputEventListeners;
+import org.expeditee.gio.input.KBMInputEvent.Key;
+import org.expeditee.gui.AttributeUtils;
+import org.expeditee.gui.AttributeValuePair;
+import org.expeditee.gui.ColorUtils;
+import org.expeditee.gui.DisplayController;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameCreator;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameIO;
+import org.expeditee.gui.FrameUtils;
+import org.expeditee.gui.FreeItems;
+import org.expeditee.gui.MessageBay;
+import org.expeditee.gui.Vector;
+import org.expeditee.io.ExpClipReader;
+import org.expeditee.io.ItemSelection;
+import org.expeditee.io.ItemSelection.ExpDataHandler;
+import org.expeditee.items.Circle;
+import org.expeditee.items.Constraint;
+import org.expeditee.items.Dot;
+import org.expeditee.items.Item;
+import org.expeditee.items.ItemUtils;
+import org.expeditee.items.Line;
+import org.expeditee.items.Picture;
+import org.expeditee.items.Text;
+import org.expeditee.items.UserAppliedPermission;
+import org.expeditee.items.XRayable;
+import org.expeditee.items.MagneticConstraint.MagneticConstraints;
+import org.expeditee.items.widgets.ButtonWidget;
+import org.expeditee.items.widgets.Widget;
+import org.expeditee.items.widgets.WidgetCorner;
+import org.expeditee.items.widgets.WidgetEdge;
+import org.expeditee.items.Item.HighlightMode;
+import org.expeditee.settings.experimental.ExperimentalFeatures;
+import org.expeditee.settings.templates.TemplateSettings;
+import org.expeditee.stats.SessionStats;
+
+public class StandardGestureActions implements GestureListener
+{
+	/** The gesture types offered by core Expeditee. */
+	public enum StandardGestureType
+	{
+		ACTION, // Make action, remove action
+		CALCULATE,
+		CHANGE_COLOUR, // F3
+		CLICK,
+		COPY,
+		CREATE_ITEM,
+		CUT,
+		DELETE,
+		DROP_DOWN, // F0 (ESC) (Seems to locate the cursor underneath the current/last item?)
+		EXTRACT_ATTRIBUTES,
+		EXTRUDE,
+		FORMAT,
+		INSERT_DATE, // F5
+		INSERT_STRING,
+		JUSTIFY,
+		LINK, // Create link, remove link, follow link
+		MAKE_CIRCLE,
+		MOVE_CURSOR,
+		NAVIGATE_FRAME, // Forward/back/next/previous frame
+		NAVIGATE_TEXT,
+		NEW_FRAMESET, // F6
+		NEXT_ITEM,
+		PAN,
+		PASTE,
+		PICK_UP,
+		PLACE,
+		REFRESH, // F12
+		ROTATE_DISPLAY_MODE, // F9
+		ROTATE_FONT_FAMILY, // F8
+		ROTATE_FONT_STYLE, // F7
+		SAVE, // F11
+		SCALE, // F1 & F2
+		SELECT_AREA,
+		SPLIT_TEXT,
+		TOGGLE_ANNOTATION, // F4
+		TOGGLE_ARROWHEAD,
+		TOGGLE_BOLD,
+		TOGGLE_ITALICS,
+		TOGGLE_ITEMS_MARK, // Show/hide the little circle indicating the item has a link and/or action
+		TOGGLE_XRAY_MODE, // F10
+		UNDO,
+		ZOOM
+	}
+	
+	private static StandardGestureActions _instance = null;
+	
+	public static StandardGestureActions getInstance()
+	{
+		if (_instance == null) {
+			_instance = new StandardGestureActions();
+		}
+		
+		return _instance;
+	}
+	
+	private HashMap<StandardGestureType, GestureType> _gestureTypes;
+
+	private HashMap<GestureType, GestureAction> _actions;
+	
+	private StandardGestureActions()
+	{
+		_gestureTypes = new HashMap<StandardGestureType, GestureType>();
+		initialiseGestureTypes();
+		_actions = new HashMap<GestureType, GestureAction>();
+		initialiseActions();
+	}
+	
+	@Override
+	public void onGesture(Gesture gesture)
+	{
+		performAction(gesture);
+	}
+	
+	private void setGestureAction(GestureType type, GestureAction action)
+	{
+		if (type == null) return;
+		
+		_actions.put(type, action);
+	}
+	
+	private GestureAction getGestureAction(GestureType type)
+	{
+		if (type == null) return null;
+		
+		return _actions.get(type);
+	}
+	
+	private void performAction(Gesture gesture)
+	{
+		GestureAction action = getGestureAction(gesture.getType());
+		if (action == null) return;
+		action.exec(gesture);
+	}
+	
+	/**
+	 * Sets up the standard gesture actions.
+	 */
+	private void initialiseActions()
+	{
+		// Set the ACTION action
+		setGestureAction(gestureType(StandardGestureType.ACTION), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item current = data.getCurrentItem();
+				// If its not linked then link it to its self
+				if (current instanceof Text) {
+					if (!current.hasAction()) {
+						String text = ((Text) current).getText().trim();
+						// first trim the annotation
+						if (text.startsWith("@")) {
+							text = text.substring(1).trim();
+						}
+						// then trim the action
+						String lowerCaseText = text.toLowerCase();
+						if (lowerCaseText.startsWith("a:")) {
+							text = text.substring("a:".length()).trim();
+						} else if (lowerCaseText.startsWith("action:")) {
+							text = text.substring("action:".length()).trim();
+
+						}
+						current.setAction(text);
+					} else {
+						// If its linked remove the link
+						current.setActions(null);
+					}
+				}
+			}
+		});
+		
+		// Set the CALCULATE action
+		setGestureAction(gestureType(StandardGestureType.CALCULATE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				Item on = ((ItemSpecificGestureData) gesture.getData()).getCurrentItem();
+				if (on != null) {
+					calculateItem(on);
+					DisplayController.requestRefresh(true);
+				}
+			}
+		});
+		
+		// Set the CHANGE_COLOUR action
+		setGestureAction(gestureType(StandardGestureType.CHANGE_COLOUR), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ChangeColourGestureData data = (ChangeColourGestureData) gesture.getData();
+				Item currentItem = data.getCurrentItem();
+				Collection<Item> currentItems = data.getCurrentItems();
+				Collection<Item> enclosure = data.getEnclosure();
+				boolean shouldSetTransparent = data.shouldSetTransparent();
+				boolean shouldSetSecondaryColour = data.shouldSetSecondaryColour();
+				if (currentItem == null && currentItems != null && currentItems.size() > 0) {
+					Collection<Item> connected = data.getEnclosure().iterator().next().getAllConnected();
+					if (connected.size() > 0) {
+						for (Item d : enclosure) {
+							if (shouldSetSecondaryColour)
+								SetGradientColor(d, shouldSetTransparent);
+							else
+								SetFillColor(d, shouldSetTransparent);
+							break;
+						}
+					}
+				} else if (currentItem != null) {
+					SetColor(currentItem, shouldSetTransparent, shouldSetSecondaryColour);
+				}
+			}
+		});
+		
+		// Set the CLICK action
+		setGestureAction(gestureType(StandardGestureType.CLICK), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ClickGestureData data = (ClickGestureData) gesture.getData();
+				click(data.getCurrentItem(), data.getCurrentItems(), data.getPosition());
+			}
+		});
+		
+		// Set the COPY action
+		setGestureAction(gestureType(StandardGestureType.COPY), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item current = data.getCurrentItem();
+				if (FreeItems.hasItemsAttachedToCursor()) {
+					ItemSelection.copyClone();
+				} else if (current instanceof Text) {
+					copyItemToClipboard(current);
+				}
+			}
+		});
+		
+		// Set the CREATE_ITEM action
+		setGestureAction(gestureType(StandardGestureType.CREATE_ITEM), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				CreateItemGestureData data = (CreateItemGestureData) gesture.getData();
+				if (data.getItemType() == ItemType.LINE) {
+					newLineAction(data.getPosition(), data.getCurrentItem());
+				} else if (data.getItemType() == ItemType.BOX) {
+					createRectangleAction();
+				}
+				// TODO: Complete. cts16
+			}
+		});
+		
+		// Set the CUT action
+		setGestureAction(gestureType(StandardGestureType.CUT), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSelection.cut();
+			}
+		});
+		
+		// Set the DELETE action
+		setGestureAction(gestureType(StandardGestureType.DELETE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				DeleteGestureData data = (DeleteGestureData) gesture.getData();
+				delete(data.getCurrentItem(), data.getCurrentItems(), data.getEnclosure(), data.getAlternateMode());
+			}
+		});
+		
+		// Set the DROP_DOWN action
+		setGestureAction(gestureType(StandardGestureType.DROP_DOWN), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				
+				// Get the enclosed items
+				Item on = FrameUtils.getCurrentItem();
+				if (on == null) {
+					Collection<Item> enclosed = FrameUtils.getCurrentItems(on);
+	
+					Collection<Item> lineEnds;
+					Item firstConnected;
+					if (enclosed != null && enclosed.size() > 0) {
+						// ensure only one dot\line is present in the list
+						lineEnds = FrameUtils.getEnclosingLineEnds();
+						firstConnected = lineEnds.iterator().next();
+					
+						// Get the last text item and drop from in
+						Item lastText = null;
+						for (Item i : enclosed) {
+							if (i instanceof Text) {
+								lastText = i;
+							}
+						}
+						
+						// Drop from the item if there was a text item in the box
+						if (lastText != null) {
+							Drop(lastText, false);
+						} else {
+							// Move to the top of the box
+							AxisAlignedBoxBounds rect = firstConnected.getBoundingBox();
+							int newX = rect.getMinX() + Text.MARGIN_LEFT;
+							int newY = Text.MARGIN_LEFT + rect.getMinY() + DisplayController.getCurrentFrame().getItemTemplate().getBoundsHeight();
+							moveCursorAndFreeItems(newX, newY);
+							// TODO can resetOffset be put inside moveCursorAndFreeItems
+							resetOffset();
+						}
+						return;
+					}
+				}
+				
+				Drop(on, false);
+			}
+		});
+		
+		// Set the EXTRACT_ATTRIBUTES action
+		setGestureAction(gestureType(StandardGestureType.EXTRACT_ATTRIBUTES), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				extractAttributes(data.getCurrentItem());
+			}
+		});
+		
+		// Set the FORMAT action
+		setGestureAction(gestureType(StandardGestureType.FORMAT), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				FormatGestureData data = (FormatGestureData) gesture.getData();
+				if (data.getHorizontal()) {
+					Actions.PerformActionCatchErrors(data.getFrame(), null, "HFormat");
+				} else {
+					Actions.PerformActionCatchErrors(data.getFrame(), null, "Format");
+				}
+			}
+		});
+		
+		// Set the INSERT_DATE action
+		setGestureAction(gestureType(StandardGestureType.INSERT_DATE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item currentItem = data.getCurrentItem();
+				Collection<Item> currentItems = data.getCurrentItems();
+				if (currentItem == null && currentItems != null && currentItems.size() > 0) {
+					Text.AddDate(data.getEnclosure().iterator().next());
+				} else {
+					Text.AddDate(currentItem);
+				}
+			}
+		});
+		
+		// Set the INSERT_STRING action
+		setGestureAction(gestureType(StandardGestureType.INSERT_STRING), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				InsertStringGestureData data = (InsertStringGestureData) gesture.getData();
+				for (char c : data.getString().toCharArray()) processChar(c, data.isShiftDown());
+			}
+		});
+		
+		// Set the JUSTIFY action
+		setGestureAction(gestureType(StandardGestureType.JUSTIFY), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				JustifyGestureData data = (JustifyGestureData) gesture.getData();
+				Text textCurrent = data.getCurrentTextItem();
+				if (textCurrent == null) {
+					for (Text t : data.getFrame().getBodyTextItems(false)) {
+						if (data.getResetWidth()) t.setWidth(null);
+						t.justify(true);
+					}
+				} else {
+					if (data.getResetWidth()) textCurrent.setWidth(null);
+					textCurrent.justify(true);
+				}
+			}
+		});
+		
+		// Set the LINK action
+		setGestureAction(gestureType(StandardGestureType.LINK), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				LinkGestureData data = (LinkGestureData) gesture.getData();
+				Item current = data.getCurrentItem();
+				boolean follow = data.getFollow();
+				// If its not linked then link it to itself
+				if (current instanceof Text && current.getLink() == null) {
+					String text = ((Text) current).getText();
+					// Ignore the annotation if there is one
+					if (text.charAt(0) == '@')
+						text = text.substring(1);
+
+					if (FrameIO.isValidFrameName(text)) {
+						current.setLink(text);
+					} else if (FrameIO.isValidFramesetName(text)) {
+						current.setLink(text + '1');
+					}
+				} else if (current != null && !follow) {
+					// If its linked remove the link
+					current.setLink(null);
+				}
+				if (current != null && current.getLink() != null && follow) {
+					Navigation.Goto(current.getAbsoluteLink());
+					return;
+				}
+			}
+		});
+		
+		// Set the MAKE_CIRCLE action
+		setGestureAction(gestureType(StandardGestureType.MAKE_CIRCLE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item current = data.getCurrentItem();
+				Text item = null;
+				// Check if its a line to be turned into a circle
+				if (current instanceof Dot && current.getLines().size() == 1) {
+					item = Item.replaceDot(current, '@');
+				} else if (current instanceof Line && current.getAllConnected().size() == 3) {
+					Item end = ((Line) current).getEndItem();
+					if (end instanceof Dot) {
+						item = Item.replaceDot(end, '@');
+					} else if (end instanceof Text) {
+						item = (Text) end;
+					}
+				}
+				if (item != null) {
+					item.setText("@c");
+					DisplayController.setCursorPosition(item.getX(), item.getY());
+					FrameUtils.setLastEdited(null);
+					Refresh();
+				}
+			}
+		});
+		
+		// Set the MOVE_CURSOR action
+		setGestureAction(gestureType(StandardGestureType.MOVE_CURSOR), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				if (gesture.isRobotic()) {
+					EcosystemManager.getInputManager().setCursorPosition(gesture.getData().getPosition());
+				} else {
+					mouseMoved(gesture.getData().getPosition());
+				}
+			}
+		});
+		
+		// Set the NAVIGATE_FRAME action
+		setGestureAction(gestureType(StandardGestureType.NAVIGATE_FRAME), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				NavigateFrameGestureData data = (NavigateFrameGestureData) gesture.getData();
+				switch (data.getNavigateTo()) {
+				case BACK_FRAME:
+					DisplayController.Back();
+					break;
+				case EARLIEST_FRAME:
+					while (DisplayController.Back());
+					break;
+				case FIRST_FRAME:
+					Navigation.ZeroFrame();
+					Navigation.NextFrame();
+					break;
+				case FORWARD_FRAME:
+					DisplayController.Forward();
+					break;
+				case LAST_FRAME:
+					Navigation.LastFrame();
+					break;
+				case LATEST_FRAME:
+					while (DisplayController.Forward());
+					break;
+				case NEXT_FRAME:
+					Navigation.NextFrame(false);
+					break;
+				case PREVIOUS_FRAME:
+					Navigation.PreviousFrame(false);
+					break;
+				case SPECIFIC_FRAME:
+					Navigation.Goto(data.getCurrentFrameset() + data.getCurrentFrameNumber());
+					break;
+				case ZERO_FRAME:
+					Navigation.ZeroFrame();
+					break;
+				default:
+					break;
+				}
+			}
+		});
+		
+		// Set the NAVIGATE_TEXT action
+		setGestureAction(gestureType(StandardGestureType.NAVIGATE_TEXT), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				NavigateTextGestureData data = (NavigateTextGestureData) gesture.getData();
+				switch (data.getNavigateTo()) {
+				case ABOVE_LINE:
+					move(Text.UP, data.getSelecting(), false);
+					break;
+				case BELOW_LINE:
+					move(Text.DOWN, data.getSelecting(), false);
+					break;
+				case LINE_END:
+					move(Text.LINE_END, data.getSelecting(), false);
+					break;
+				case LINE_HOME:
+					move(Text.LINE_HOME, data.getSelecting(), false);
+					break;
+				case NEXT_CHARACTER:
+					move(Text.RIGHT, data.getSelecting(), false);
+					break;
+				case NEXT_TEXT_ITEM:
+					NextTextItem(data.getCurrentItem(), true);
+					break;
+				case NEXT_WORD:
+					DisplayController.setTextCursor((Text) data.getCurrentItem(), Text.RIGHT, false, data.getSelecting(), true, true);
+					break;
+				case PARAGRAPH_END:
+					move(Text.END, data.getSelecting(), true);
+					break;
+				case PARAGRAPH_HOME:
+					move(Text.HOME, data.getSelecting(), true);
+					break;
+				case PREVIOUS_CHARACTER:
+					move(Text.LEFT, data.getSelecting(), false);
+					break;
+				case PREVIOUS_TEXT_ITEM:
+					NextTextItem(data.getCurrentItem(), false);
+					break;
+				case PREVIOUS_WORD:
+					DisplayController.setTextCursor((Text) data.getCurrentItem(), Text.LEFT, false, data.getSelecting(), true, true);
+					break;
+				default:
+					break;
+				}
+			}
+		});
+		
+		// Set the NEW_FRAMESET action
+		setGestureAction(gestureType(StandardGestureType.NEW_FRAMESET), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				if (data.getCurrentItem() != null) CreateFrameset(data.getCurrentItem());
+			}
+		});
+		
+		// Set the NEXT_ITEM action
+		setGestureAction(gestureType(StandardGestureType.NEXT_ITEM), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				//GestureData data = (GestureData) gesture.getData();
+				// TODO: Complete. cts16
+			}
+		});
+		
+		// Set the PAN action
+		setGestureAction(gestureType(StandardGestureType.PAN), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				PanGestureData data = (PanGestureData) gesture.getData();
+				Misc.pan(DisplayController.getCurrentFrame(), data.getPanDelta().x, data.getPanDelta().y);
+			}
+		});
+		
+		// Set the PASTE action
+		setGestureAction(gestureType(StandardGestureType.PASTE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSelection.paste();
+			}
+		});
+		
+		// Set the PICK_UP action
+		setGestureAction(gestureType(StandardGestureType.PICK_UP), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				PickUpGestureData data = (PickUpGestureData) gesture.getData();
+				if (!data.wasDragged()) {
+					if (data.getCurrentItem() != null) {
+						handlePickup(data.getCurrentItem(), data.getPosition(), data.getCopy(), data.getExtrude());
+					} else if (data.getCurrentItems() != null) {
+						handlePickup(data.getCurrentItems(), data.getEnclosure(), data.getPosition(), data.getCopy());
+					}
+				} else {
+					Item item = data.getCurrentItem();
+					if  (item instanceof Text) {
+						Text text = (Text) item;
+						text.setSelectionStart(data.getDraggedFrom());
+						text.setSelectionEnd(data.getPosition());
+						text.setSelectionColour(data.getCopy() ? Text.RANGE_COPY_COLOUR : Text.RANGE_CUT_COLOUR);
+						DisplayController.setTextCursor(text, Text.NONE, false, false, false, false);
+						if (data.getFinishedDragging()) {
+							pickupRange(text, data.getPosition(), data.getCopy(), data.getInheritAttributes());
+						} else {
+							DisplayController.requestRefresh(true);
+						}
+					} else if (item instanceof Picture && data.getCopy()) {
+						Picture picture = (Picture) item;
+						picture.setStartCrop(data.getDraggedFrom());
+						picture.setEndCrop(data.getPosition());
+						picture.setShowCrop(true);
+						picture.setHighlightMode(Item.HighlightMode.None);
+						picture.setHighlightColor(Item.DEPRESSED_HIGHLIGHT);
+						if (data.getFinishedDragging()) {
+							if (picture.isCropTooSmall()) return;
+							Picture cropped = picture.copy();
+							cropped.setParent(null);
+							// move the cropped image to the cursor
+							int width = cropped.getWidth();
+							int height = cropped.getHeight();
+							if(cropped.getSource().getX() + width < data.getPosition().x) {
+								cropped.getSource().setX(data.getPosition().x - width);
+							}
+							if(cropped.getSource().getY() + height < data.getPosition().y) {
+								cropped.getSource().setY(data.getPosition().y - height);
+							}
+							pickup(cropped);
+							// MIKE put the code below up here
+							picture.clearCropping();
+							FrameGraphics.changeHighlightMode(picture, HighlightMode.None);
+						}
+						DisplayController.requestRefresh(true);
+					}
+				}
+			}
+		});
+		
+		// Set the PLACE action
+		setGestureAction(gestureType(StandardGestureType.PLACE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				PickUpGestureData data = (PickUpGestureData) gesture.getData();
+				if (data.getCopy()) {
+					copyPlace(data.getCurrentItem());
+				} else {
+					if (doMerging(data.getCurrentItem())) {
+						deleteItemsAction(data.getCurrentItem());
+					} else {
+						anchorFreeItemsAction(data.getCurrentItems());
+					}
+				}
+			}
+		});
+		
+		// Set the REFRESH action
+		setGestureAction(gestureType(StandardGestureType.REFRESH), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				RefreshGestureData data = (RefreshGestureData) gesture.getData();
+				if (data.shouldReloadFrameFirst()) {
+				    MessageBay.displayMessage("Forced reload before forced refresh");
+				    DisplayController.Reload(DisplayController.TwinFramesSide.LEFT);
+				}
+				Refresh(data.shouldRefreshFrameSize());
+			}
+		});
+		
+		// Set the ROTATE_DISPLAY_MODE action
+		setGestureAction(gestureType(StandardGestureType.ROTATE_DISPLAY_MODE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				DisplayController.rotateAudienceModes();
+			}
+		});
+		
+		// Set the ROTATE_FONT_FAMILY action
+		setGestureAction(gestureType(StandardGestureType.ROTATE_FONT_FAMILY), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item currentItem = data.getCurrentItem();
+				Collection<Item> currentItems = data.getCurrentItems();
+				if (currentItem == null && currentItems != null && currentItems.size() > 0) {
+					ToggleFontFamily(data.getEnclosure().iterator().next());
+				} else if (currentItem != null) {
+					ToggleFontFamily(currentItem);
+				}
+			}
+		});
+		
+		// Set the ROTATE_FONT_STYLE action
+		setGestureAction(gestureType(StandardGestureType.ROTATE_FONT_STYLE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item currentItem = data.getCurrentItem();
+				Collection<Item> currentItems = data.getCurrentItems();
+				if (currentItem == null && currentItems != null && currentItems.size() > 0) {
+					ToggleFontStyle(data.getEnclosure().iterator().next());
+				} else if (currentItem != null) {
+					ToggleFontStyle(currentItem);
+				}
+			}
+		});
+		
+		// Set the SAVE action
+		setGestureAction(gestureType(StandardGestureType.SAVE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				Save();
+			}
+		});
+		
+		// Set the SCALE action
+		setGestureAction(gestureType(StandardGestureType.SCALE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ScaleGestureData data = (ScaleGestureData) gesture.getData();
+				boolean scaleAroundCursor = data.getPosition() != null;
+				int scaleFactor = data.getScaleFactor();
+				Item currentItem = data.getCurrentItem();
+				Collection<Item> currentItems = data.getCurrentItems();
+				if (currentItem == null && currentItems != null && currentItems.size() > 0) {
+					SetSize(data.getEnclosure().iterator().next(), scaleFactor, false, true, scaleAroundCursor);
+				} else if (currentItem != null) {
+					SetSize(currentItem, scaleFactor, true, false, scaleAroundCursor);
+					if (currentItem instanceof Text) {
+						DisplayController.setTextCursor((Text) currentItem, Text.NONE, true, false, false, true);
+					}
+				}
+			}
+		});
+		
+		// Set the SELECT_AREA action
+		setGestureAction(gestureType(StandardGestureType.SELECT_AREA), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				SelectAreaGestureData data = (SelectAreaGestureData) gesture.getData();
+				Item item = data.getCurrentItem();
+				if (item != null && item instanceof Text) {
+					Text text = (Text) item;
+					text.setSelectionStart(data.getDraggedFrom());
+					text.setSelectionEnd(data.getPosition());
+					text.setSelectionColour(Text.RANGE_SELECT_COLOUR);
+					DisplayController.setTextCursor(text, Text.NONE, false, false, false, false);
+					DisplayController.requestRefresh(true);
+				}
+			}
+		});
+		
+		// Set the SPLIT_TEXT action
+		setGestureAction(gestureType(StandardGestureType.SPLIT_TEXT), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Text text = data.getCurrentTextItem();
+				if (text == null) return;
+				List<String> textLines = text.getTextList();
+				if (textLines.size() <= 1) return;
+				// remove all except the first line of text from the item being split
+				text.setText(textLines.get(0));
+				int y = text.getY();
+				Frame parent = text.getParent();
+				for (int i = 1; i < textLines.size(); i++) {
+					Text newText = text.copy();
+					newText.setText(textLines.get(i));
+					y += newText.getBoundsHeight();
+					newText.setY(y);
+					// update the items ID to prevent conflicts with the current frame
+					if (parent != null) {
+						newText.setID(parent.getNextItemID());
+						parent.addItem(newText);
+					}
+				}
+			}
+		});
+		
+		// Set the TOGGLE_ANNOTATION action
+		setGestureAction(gestureType(StandardGestureType.TOGGLE_ANNOTATION), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item currentItem = data.getCurrentItem();
+				Collection<Item> currentItems = data.getCurrentItems();
+				if (currentItem == null && currentItems != null && currentItems.size() > 0) {
+					ToggleAnnotation(data.getEnclosure().iterator().next());
+				} else if (currentItem != null) {
+					ToggleAnnotation(currentItem);
+				}
+			}
+		});
+		
+		// Set the TOGGLE_ARROWHEAD action
+		setGestureAction(gestureType(StandardGestureType.TOGGLE_ARROWHEAD), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item item = data.getCurrentItem();
+				if (item instanceof Line) {
+					((Line) item).toggleArrow();
+					DisplayController.requestRefresh(true);
+				}
+			}
+		});
+		
+		// Set the TOGGLE_BOLD action
+		setGestureAction(gestureType(StandardGestureType.TOGGLE_BOLD), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item current = data.getCurrentItem();
+				if (current instanceof Text) {
+					((Text) current).toggleBold();
+				}
+			}
+		});
+		
+		// Set the TOGGLE_ITALICS action
+		setGestureAction(gestureType(StandardGestureType.TOGGLE_ITALICS), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item current = data.getCurrentItem();
+				if (current instanceof Text) {
+					((Text) current).toggleItalics();
+				}
+			}
+		});
+		
+		// Set the TOGGLE_ITEMS_MARK action
+		setGestureAction(gestureType(StandardGestureType.TOGGLE_ITEMS_MARK), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ItemSpecificGestureData data = (ItemSpecificGestureData) gesture.getData();
+				Item current = data.getCurrentItem();
+				if (current == null)
+					return;
+				if (current != null && !current.hasPermission(UserAppliedPermission.full)) {
+					MessageBay.displayMessage("Insufficient permission toggle the items mark");
+					return;
+				}
+				boolean newValue = !(current.getLinkMark() || current.getActionMark());
+				current.setLinkMark(newValue);
+				current.setActionMark(newValue);
+			}
+		});
+		
+		// Set the TOGGLE_XRAY_MODE action
+		setGestureAction(gestureType(StandardGestureType.TOGGLE_XRAY_MODE), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				DisplayController.ToggleXRayMode();
+			}
+		});
+		
+		// Set the UNDO action
+		setGestureAction(gestureType(StandardGestureType.UNDO), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				UndoGestureData data = (UndoGestureData) gesture.getData();
+				if (data.getRedo()) {
+					DisplayController.getCurrentFrame().redo();
+				} else {
+					DisplayController.getCurrentFrame().undo();
+				}
+			}
+		});
+		
+		// Set the ZOOM action
+		setGestureAction(gestureType(StandardGestureType.ZOOM), new GestureAction() {
+			@Override
+			public void exec(Gesture gesture) {
+				ZoomGestureData data = (ZoomGestureData) gesture.getData();
+				zoomFrameIfEnabled(DisplayController.getCurrentFrame(), data.getScaleFactor(), data.getPosition());
+			}
+		});
+		
+	}
+	
+	/** Initialises the set of gesture types. */
+	private void initialiseGestureTypes()
+	{
+		for (StandardGestureType type : StandardGestureType.values()) {
+			GestureType gestureType;
+			if ((gestureType = GestureType.register(type.toString())) == null) {
+				gestureType = GestureType.get(type.toString());
+			}
+			_gestureTypes.put(type, gestureType);
+		}
+	}
+	
+	/** Gets the gesture type associated with the given standard type. */
+	public GestureType gestureType(StandardGestureType type)
+	{
+		if (type == null) return null;
+		
+		return _gestureTypes.get(type);
+	}
+
+	/* TODO: EVERYTHING BELOW HERE IS ONLY TEMPORARILY SITUATED IN THIS CLASS
+	 *  
+	 * The following methods were mostly copy-pasted here from the old FrameKeyboardActions
+	 * and FrameMouseActions classes (to enable quick change-over to the new input system).
+	 * Really they should be relocated to more suitable homes based on their function. 
+	 */
+	
+	/**
+	 * Picks up an item on a frame.
+	 * 
+	 * @param toGrab
+	 *            item to be picked up
+	 * @param removeItem
+	 *            true if the item should be removed from the frame
+	 */
+	public static void pickup(Item toGrab) {
+		if (toGrab.isFrameName())
+			return;
+
+		if (!toGrab.hasPermission(UserAppliedPermission.full)) {
+			if (toGrab.getEditTarget() != toGrab) {
+				pickup(toGrab.getEditTarget());
+			} else {
+				MessageBay.displayMessage("Insufficient permission pickup the item");
+			}
+			return;
+		}
+
+		if (toGrab instanceof Circle)
+			toGrab.setHighlightMode(HighlightMode.Connected);
+		// Dont set the highlight mode if a vector is being picked up
+		else if (toGrab.isVisible()) {
+			toGrab.setHighlightMode(HighlightMode.Normal);
+		}
+
+		// Brook: If the widget corner is being picked up. Instead refer to
+		// picking up the edge for fixed-sized widgets so it is not so confusing
+		if (toGrab instanceof WidgetCorner) {
+			WidgetCorner wc = (WidgetCorner) toGrab;
+			if (wc.getWidgetSource().isFixedSize()) {
+				for (Item i : toGrab.getConnected()) {
+					if (i instanceof WidgetEdge) {
+						toGrab = i;
+						break;
+					}
+				}
+			}
+		}
+		pickup(toGrab.getConnected());
+	}
+
+	// TODO: Review if these are needed as they ugly up the code. cts16
+	static int _offX, _offY;
+	
+	/**
+	 * Picks up a group of items on a frame.
+	 * 
+	 * @param toGrab
+	 * 		The items to pick up.
+	 */
+	public static void pickup(Collection<Item> toGrab) {
+		if (toGrab == null || toGrab.size() == 0)
+			return;
+
+		boolean bReparse = false;
+		boolean bRecalculate = false;
+
+		Frame currentFrame = DisplayController.getCurrentFrame();
+		String currentFrameName = currentFrame.getName();
+		Iterator<Item> iter = toGrab.iterator();
+		while (iter.hasNext()) {
+			Item i = iter.next();
+			if (!i.hasPermission(UserAppliedPermission.full)) {
+				iter.remove();
+				continue;
+			}
+			if (i.equals(_lastHighlightedItem)) _lastHighlightedItem = null;
+
+			bRecalculate |= i.recalculateWhenChanged();
+			// i.setSelectedMode(SelectedMode.None);
+			// Check if it has a relative link if so make it absolute
+			i.setAbsoluteLink();
+			// parent may be null
+			if (i.getParent() != null) {
+				i.getParent().removeItem(i);
+				if (currentFrameName.equals(i.getParent().getName()))
+					i.setParent(null);
+			}
+			FreeItems.getInstance().add(i);
+			i.setFloating(true);
+			// If its a vector pick up a copy of the stuff on the vector frame
+			if (i.hasVector()) {
+				bReparse = true;
+
+				Frame overlayFrame = FrameIO.LoadFrame(i.getAbsoluteLink());
+				Collection<Item> copies = ItemUtils.CopyItems(overlayFrame
+						.getNonAnnotationItems(false), i.getVector());
+				for (Item copy : copies) {
+					FreeItems.getInstance().add(copy);
+					copy.setEditTarget(i);
+					copy.setFloating(true);
+					copy.setParent(null);
+					// copy.setHighlightMode(HighlightMode.Connected);
+				}
+			}
+		}
+		currentFrame.change();
+
+		_lastHighlightedItem = null;
+		updateCursor();
+
+		// if there are multiple items in the list, determine which to use for
+		// offset calculations
+		if (toGrab.size() > 1) {
+			for (Item i : toGrab) {
+				// MIKE: Movement goes haywire if these are removed because Line
+				// class returns 0 for getX
+				if (!(i instanceof Line) && !(i instanceof XRayable)) {
+					_offX = DisplayController.getMouseX() - i.getX() + i.getOffset().x;
+					_offY = DisplayController.getMouseY() - i.getY() + i.getOffset().y;
+
+					// make the offset item the first item in the list (so
+					// move method knows which item to use)
+					FreeItems.getInstance().set(FreeItems.getInstance().indexOf(i), FreeItems.getInstance().get(0));
+					FreeItems.getInstance().set(0, i);
+					break;
+				}
+			}
+
+			move(FreeItems.getInstance(), EcosystemManager.getInputManager().getCursorPosition());
+			ItemUtils.EnclosedCheck(toGrab);
+			// otherwise, just use the first item
+		} else if (toGrab.size() == 1) {
+			Item soleItem = toGrab.iterator().next();
+			_offX = DisplayController.getMouseX() - soleItem.getX() + soleItem.getOffset().x;
+			_offY = DisplayController.getMouseY() - soleItem.getY() + soleItem.getOffset().y;
+			// Now call move so that if we are on a message in the message box
+			// It doesn't appear up the top of the screen!!
+			move(toGrab, EcosystemManager.getInputManager().getCursorPosition());
+		} else {
+			MessageBay.displayMessage("Insufficient permission to pickup the items");
+		}
+		if (bReparse)
+			FrameUtils.Parse(currentFrame, false, false);
+		else
+			currentFrame.notifyObservers(bRecalculate);
+
+		DisplayController.requestRefresh(true);
+	}
+
+	/**
+	 * Updates the current mouse cursor to whatever it should be. i.e. Hidden
+	 * when rubber-banding lines, otherwise default (arrow)
+	 */
+	public static void updateCursor() {
+		if (FreeItems.rubberBanding()) {
+			DisplayController.setCursor(Item.HIDDEN_CURSOR);
+			return;
+		}
+		// This is to make sure the TEXT_CURSOR doesn't get inadvertently turned off!
+		Item on = FrameUtils.getCurrentItem();
+		if (on != null && on instanceof Text) {
+			return;
+		}
+		DisplayController.setCursor(Item.DEFAULT_CURSOR);
+	}
+
+	private static Text _toRemove = null;
+
+	public static void processChar(char ch, boolean isShiftDown)
+	{
+		Navigation.ResetLastAddToBack();
+		Item on = FrameUtils.getCurrentItem();
+
+		// permission check
+		if (on != null && !on.hasPermission(UserAppliedPermission.full)) {
+			MessageBay.displayMessage("Insufficient permission to edit this item");
+			return;
+		}
+
+		// TODO: What does this do? cts16
+		if (isShiftDown && MagneticConstraints.getInstance().keyHit(-ch, on)) {
+			return;
+		} else if (MagneticConstraints.getInstance().keyHit(ch, on)) {
+			return;
+		}
+
+		if (_toRemove != null && on != _toRemove) {
+			assert (_toRemove.getLength() == 0);
+			// This line is to protect mistaken removal of items if there is a bug...
+			if (_toRemove.getLength() == 0) DisplayController.getCurrentFrame().removeItem(_toRemove);
+		}
+		_toRemove = null;
+
+		// ignore delete and backspace if in free space
+		if ((on == null || !(on instanceof Text)) && (ch == '\b' || ch == '\t' || ch == Text.DELETE_CHARACTER)) return;
+
+		SessionStats.TypedChar(ch);
+
+		// check for dot's being replaced with text
+		if (on != null && on instanceof Dot && !(on instanceof WidgetCorner)) {
+			if (ch == '\b' || ch == Text.DELETE_CHARACTER) {
+				return;
+			}
+			Item.replaceDot((Item) on, ch);
+			return;
+		}
+
+		// only text can interact with keyboard events
+		if (on != null && !(on instanceof Text)) on = null;
+
+		// DisplayIO.UpdateTitle();
+
+		Text text = (Text) on;
+		// if this text is empty but has not been removed (such as from ESC-pushdown)
+		if (text != null && text.isEmpty() && (ch == '\b' || ch == Text.DELETE_CHARACTER)) {
+			if (text.getLines().size() > 0)
+				Text.replaceText(text);
+			else {
+				DisplayController.setCursor(Item.DEFAULT_CURSOR);
+			}
+			return;
+		}
+
+		// if the user is in free space, create a new text item
+		// MikeSays: Why do we have to check is highlighted... doing so causes
+		// problems if you type characters too fast, they turn into multiple text
+		// items. ie. JK together on the Linux laptop.
+		if (on == null /* || !on.isHighlighted() */) {
+			// DisplayIO.UpdateTitle();
+			text = Text.createText(ch);
+			text.justify(false);
+
+			FrameUtils.setLastEdited(text);
+			DisplayController.setTextCursor(text, Text.NONE);
+			DisplayController.requestRefresh(true);
+			return;
+		} else {
+			FrameUtils.setLastEdited(text);
+		}
+
+		DisplayController.setTextCursor(text, Text.NONE);
+		insertCharacterAction(text, isShiftDown, ch);
+
+		// This repaint is needed for WINDOWS only?!?!? Mike is not sure why!
+		//if (ch == Text.DELETE_CHARACTER)
+		DisplayController.requestRefresh(true);
+
+		// a change has occured to the Frame
+		text.getParent().setChanged(true);
+
+		// check that the Text item still exists (hasn't been deleted\backspaced
+		// away)
+		if (text.isEmpty()) {
+			_toRemove = text;
+
+			if (text.hasAction())
+				text.setActionMark(true);
+			else if (text.getLink() != null)
+				text.setLinkMark(true);
+			else if (text.getLines().size() > 0)
+				Item.replaceText(text);
+			else {
+				// DisplayIO.getCurrentFrame().removeItem(text);
+				DisplayController.setCursor(Item.DEFAULT_CURSOR);
+			}
+		}
+	}
+	
+	public static Text insertCharacterAction(Text text, boolean isShiftDown, final char c)
+	{
+		float oldY = DisplayController.getFloatMouseY();
+		Point newMouse = null;
+		if (c == '\t') {
+			if (isShiftDown) {
+				newMouse = text.removeTab(c, DisplayController.getFloatMouseX(), DisplayController.getFloatMouseY());
+			} else {
+				newMouse = text.insertTab(c, DisplayController.getFloatMouseX(), DisplayController.getFloatMouseY());
+			}
+		} else {
+			newMouse = text.insertChar(c, DisplayController.getFloatMouseX(), DisplayController.getFloatMouseY());
+		}
+		
+		// check if the user hit enter and the cursor is now on another text item
+		if (oldY < newMouse.y) {
+			AxisAlignedBoxBounds rect = text.getBoundingBox();
+
+			Item justBelow = FrameUtils.onItem(DisplayController.getCurrentFrame(),
+					text.getX() + 10, rect.getMinY() + rect.getHeight() + 1, false);
+
+			// Dont drop unless
+			if (justBelow != null && justBelow instanceof Text && justBelow != text) {
+				// Drop all the items below it down!
+				// Get the list of items that must be dropped
+				List<Text> column = DisplayController.getCurrentFrame().getColumn(text);
+				FrameUtils.Align(column, false, 0);
+			}
+		}
+
+		DisplayController.setCursorPosition(newMouse.x, newMouse.y, false);
+		return text;
+	}
+
+	/**
+	 * Moves the items to the current mouse position (plus the current offset)
+	 * 
+	 * @param toMove
+	 */
+	public static void move(Collection<Item> toMove, Point to)
+	{
+		move(toMove, to, false);
+	}
+
+	static void move(Collection<Item> toMove, Point to, boolean cursor)
+	{
+		Item firstDot = toMove.iterator().next();
+		
+		int deltax = (to.x - (cursor ? 0 : _offX)) - firstDot.getX();
+		int deltay = (to.y - (cursor ? 0 : _offY)) - firstDot.getY();
+		
+		// Cache the position of all items before moving any as constraints
+		// may move the items before we get to them, causing jumping
+		HashMap<Item, Point> preMovePositions = new HashMap<Item, Point>();
+		for (Item move : toMove) {
+			preMovePositions.put(move, move.getPosition());
+		}
+		
+		for (Item move : toMove) {
+			Point pos = preMovePositions.get(move);
+			move.setPosition(pos.x + deltax, pos.y + deltay);
+
+			if (!cursor && move instanceof Text) {
+				((Text) move).setAlpha(FrameUtils.hasCurrentItem() && FreeItems.getInstance().size() > 0 ? 60 : -1);
+			}
+		}
+
+		DisplayController.requestRefresh(true);
+	}
+
+	/**
+	 * Returns true if the mouse moved during TDFC. This will happen if there is
+	 * a start annotation item on the frame.
+	 * 
+	 * @param linker
+	 * @return
+	 */
+	public static boolean tdfc(Item linker) throws RuntimeException 
+	{
+		// if this is a non-usable item
+		if (linker.getID() < 0)
+			return false;
+
+		// Check if its an image that can be resized to fit a box around it
+		String text = linker.getText();
+		boolean isVector = text.equals("@v") || text.equals("@av");
+		boolean isFrameImage = text.equals("@f");
+		boolean isBitmap = false; // text.equals("@b");
+
+		if (isVector || isFrameImage || isBitmap) {
+			Collection<Item> enclosure = FrameUtils.getEnclosingLineEnds(linker.getPosition());
+			if (enclosure != null) {
+				for (Item i : enclosure) {
+					if (i.isLineEnd() && i.isEnclosed()) {
+						if (!isVector) DisplayController.getCurrentFrame().removeAllItems(enclosure);
+						AxisAlignedBoxBounds rect = i.getEnclosedBox();
+						long width = Math.round(rect.getWidth());
+						if (isVector) {
+							NumberFormat nf = Vector.getNumberFormatter();
+							linker.setText(linker.getText() + ": " + nf.format((width / DisplayController.getFramePaintArea().getWidth())));
+						} else {
+							linker.setText(linker.getText() + ": " + width);
+						}
+
+						linker.setPosition(new Point(rect.getMinX(), rect.getMinY()));
+						linker.setThickness(i.getThickness());
+						linker.setBorderColor(i.getColor());
+						break;
+					}
+				}
+				if (!isVector)
+					deleteItems(enclosure, false);
+			}
+		}
+
+		boolean mouseMoved;
+
+		linker.getParent().setChanged(true);
+
+		Frame next = FrameIO.CreateNewFrame(linker, null);
+
+		linker.setLink("" + next.getNumber());
+
+		for (Item i : next.getTextItems()) {
+			// Set the link for @Parent annotation item if one
+			if (ItemUtils.startsWithTag(i, ItemUtils.TAG_PARENT)
+					&& i.getLink() == null) {
+				Frame parent = linker.getParentOrCurrentFrame();
+				i.setLink(parent.getName());
+			} else if (ItemUtils.startsWithTag(i, ItemUtils.TAG_BACKUP, false)) {
+				// Delink backup tag if it is on the frame
+				i.setLink(null);
+			}
+		}
+
+		FrameUtils.DisplayFrame(next, true, true);
+		FrameUtils.setTdfcItem(linker);
+
+		mouseMoved = next.moveMouseToDefaultLocation();
+		// this needs to be done if the user doesnt move the mouse before doing
+		// tdfc while the cursor is set to the text cursor
+		DisplayController.setCursor(Item.DEFAULT_CURSOR);
+		// This needs to be done in case there was a @start on the frame which
+		// triggers changed to be set to true when it should stay as false
+		next.setChanged(false);
+		return mouseMoved;
+	}
+
+	public static void resetOffset() {
+		if (FreeItems.hasItemsAttachedToCursor()) {
+			_offX = DisplayController.getMouseX() - FreeItems.getInstance().get(0).getX() + FreeItems.getInstance().get(0).getOffset().x;
+			_offY = DisplayController.getMouseY() - FreeItems.getInstance().get(0).getY() + FreeItems.getInstance().get(0).getOffset().y;
+		}
+	}
+
+	/**
+	 * Forces a re-parse and repaint of the current Frame.
+	 */
+	public static void Refresh() {
+		Refresh(false);
+	}
+
+	/**
+	 * Forces a re-parse and repaint of the current Frame.
+	 */
+	public static void Refresh(boolean refreshFrameSize) {
+		Frame currentFrame = DisplayController.getCurrentFrame();
+
+		if (refreshFrameSize) {
+			currentFrame.refreshSize();
+		}
+
+		// Refresh widgets that use its self as a data source
+		currentFrame.notifyObservers(true);
+
+		if (FrameIO.isProfileFrame(currentFrame)) {
+			// TODO ensure that users can not delete the first frame in a frameset...
+			// TODO handle the case when users manually delete the first frame in a frameset from the filesystem
+			Frame profile = FrameIO.LoadFrame(currentFrame.getFramesetName() + "1");
+			assert (profile != null);
+			FrameUtils.Parse(currentFrame);
+			FrameUtils.ParseProfile(profile);
+		} else {
+			FrameUtils.Parse(currentFrame);
+		}
+		// Need to update the cursor for when text items change to @b pictures
+		// etc and the text cursor is showing
+		updateCursor();
+		refreshHighlights();
+		DisplayController.requestRefresh(false);
+	}
+
+	/**
+	 * Saves the current frame.
+	 */
+	public static void Save() {
+		Frame current = DisplayController.getCurrentFrame();
+		current.change();
+		FrameIO.SaveFrame(current, true, true);
+	}
+
+	public static final String DEFAULT_NEW_ITEM_TEXT = "";
+
+	public static String getAutoBullet(String s) {
+		return getBullet(s, true);
+	}
+
+	private static String getBullet(String s, boolean nextBullet) {
+		String newItemText = DEFAULT_NEW_ITEM_TEXT;
+
+		if (s == null) return newItemText;
+		
+		/*
+		 * Item i = ItemUtils.FindTag(DisplayIO.getCurrentFrame().getItems(),
+		 * "@NoAutoBullets"); if (i != null) return newItemText;
+		 */
+		// Separate the space at the start of the text item
+		String preceedingSpace = "";
+		for (int i = 0; i < s.length(); i++) {
+			if (!Character.isSpaceChar(s.charAt(i))) {
+				preceedingSpace = s.substring(0, i);
+				s = s.substring(i);
+				break;
+			}
+		}
+
+		// figure out the type of the text item
+		// This allows us to do auto bulleting
+		if (s != null && s.length() > 1) {
+			// First check for text beginning with * @ # etc
+			// These are simple auto bullets
+			if (!Character.isLetterOrDigit(s.charAt(0))
+					&& !Character.isSpaceChar(s.charAt(0))) {
+				if (Text.isBulletChar(s.charAt(0))) {
+					int nonSpaceIndex = 1;
+					// Find the end of the bullet and space after the bullet
+					while (nonSpaceIndex < s.length()
+							&& s.charAt(nonSpaceIndex) == ' ') {
+						nonSpaceIndex++;
+					}
+					// we must have a special char followed by >= 1 space
+					if (nonSpaceIndex > 1)
+						newItemText = s.substring(0, nonSpaceIndex);
+				}
+				// Auto numbering and lettering
+			} else {
+				if (Character.isDigit(s.charAt(0))) {
+					newItemText = getAutoNumber(s, nextBullet);
+					// Auto lettering
+				} else if (Character.isLetter(s.charAt(0))) {
+					newItemText = getAutoLetter(s, nextBullet);
+				}
+			}
+		}
+		return preceedingSpace + newItemText;
+	}
+
+	/**
+	 * Gets the string to be used to start the next auto lettered text item.
+	 * 
+	 * @param s
+	 *            the previous text items
+	 * @return the initial text for the new text item
+	 */
+	private static String getAutoLetter(String s, boolean nextBullet) {
+		String newItemText = DEFAULT_NEW_ITEM_TEXT;
+
+		int nonLetterIndex = 1;
+
+		if (isAutoNumberOrLetterChar(s.charAt(nonLetterIndex))) {
+
+			// Now search for the next non space character
+			int nonSpaceIndex = nonLetterIndex + 1;
+			while (nonSpaceIndex < s.length() && s.charAt(nonSpaceIndex) == ' ') {
+				nonSpaceIndex++;
+			}
+
+			// If there was a space then we have reached the end of our auto
+			// text
+			if (nonSpaceIndex > nonLetterIndex + 1) {
+				if (nextBullet)
+					newItemText = nextLetterSequence(s.substring(0,
+							nonLetterIndex))
+							+ s.substring(nonLetterIndex, nonSpaceIndex);
+				else
+					newItemText = s.substring(0, nonSpaceIndex);
+			}
+		}
+		return newItemText;
+	}
+
+	/**
+	 * Gets the string to be used to start the next auto numbered text item.
+	 * 
+	 * @param s
+	 *            the previous text item
+	 * @return the beginning of the next auto numbered text item
+	 */
+	private static String getAutoNumber(String s, boolean nextBullet) {
+		String newItemText = DEFAULT_NEW_ITEM_TEXT;
+
+		int nonDigitIndex = 1;
+		while (Character.isDigit(s.charAt(nonDigitIndex))) {
+			nonDigitIndex++;
+
+			if (nonDigitIndex + 1 >= s.length())
+				return DEFAULT_NEW_ITEM_TEXT;
+		}
+
+		if (isAutoNumberOrLetterChar(s.charAt(nonDigitIndex))) {
+
+			// we must have a number followed one non letter
+			// then one or more spaces
+			int nonSpaceIndex = nonDigitIndex + 1;
+			while (nonSpaceIndex < s.length() && s.charAt(nonSpaceIndex) == ' ') {
+				nonSpaceIndex++;
+			}
+
+			if (nonSpaceIndex > nonDigitIndex + 1) {
+				if (nextBullet)
+					newItemText = (Integer.parseInt(s.substring(0,
+							nonDigitIndex)) + 1)
+							+ s.substring(nonDigitIndex, nonSpaceIndex);
+				else
+					newItemText = s.substring(0, nonSpaceIndex);
+			}
+		}
+		return newItemText;
+	}
+
+	private static boolean isAutoNumberOrLetterChar(char c) {
+		return c == ':' || c == '-' || c == '.' || c == ')' || c == '>';
+	}
+
+	/**
+	 * Gets the next letter sequence for a given string to be used in auto
+	 * lettering.
+	 * 
+	 * @param s
+	 *            a sequence of letters
+	 * @return the next sequence of letters
+	 */
+	static private String nextLetterSequence(String s) {
+		if (s.length() > 1)
+			return s;
+
+		if (s.equals("z"))
+			return "a";
+
+		return (char) ((int) s.charAt(0) + 1) + "";
+	}
+
+	public static String getBullet(String s) {
+		return getBullet(s, false);
+	}
+
+	/** true if lastItem only has highlighting removed when a new item is highlighted. */
+	private static boolean _lastHoldsHighlight = false;
+
+	public static void setHighlightHold(boolean hold) {
+		_lastHoldsHighlight = hold;
+	}
+
+	public static final int CONTEXT_FREESPACE = 0;
+
+	public static final int CONTEXT_AT_TEXT = 1;
+
+	public static final int CONTEXT_AT_LINE = 2;
+
+	public static final int CONTEXT_AT_DOT = 3;
+
+	public static final int CONTEXT_AT_ENCLOSURE = 4;
+
+	/** the current context of the cursor. */
+	private static int _context = 0;
+
+	/** keeps track of the last highlighted Item. */
+	private static Item _lastHighlightedItem = null;
+	
+	private static boolean _forceArrowCursor = true;
+
+	public static void refreshHighlights() {
+		// ByMike: Get the item the mouse is hovering over
+		Item click = FrameUtils.getCurrentItem();
+		Item on = null;
+		
+		// System.out.println(click);
+		if (click != null) {
+			on = click;
+			// set the context
+			if (on instanceof Line)
+				_context = CONTEXT_AT_LINE;
+			else if (on instanceof Dot)
+				_context = CONTEXT_AT_DOT;
+			else if (on instanceof Text) {
+				_context = CONTEXT_AT_TEXT;
+			}
+		} else {
+			_context = CONTEXT_FREESPACE;
+		}
+
+		// if the user is pointing at an item, highlight it
+		if (on != null && !FreeItems.getInstance().contains(on)) {
+			// if the user can spot-weld, show the virtual spot
+			if (FreeItems.getInstance().size() == 2 && on instanceof Line) {
+				Line line = (Line) on;
+				Item freeItem0 = FreeItems.getInstance().get(0);
+				Item freeItem1 = FreeItems.getInstance().get(1);
+				Item lineEnd = freeItem0.isLineEnd() ? freeItem0 : (freeItem1.isLineEnd() ? freeItem1 : null);
+				if (lineEnd != null) {
+					line.showVirtualSpot(lineEnd, DisplayController.getMouseX(), DisplayController.getMouseY());
+				} else {
+					// The user is pointing at another point or text item etc
+					FrameGraphics.changeHighlightMode(on, Item.HighlightMode.Normal);
+				}
+			} else {
+				// FrameGraphics.ChangeSelectionMode(on,
+				// Item.SelectedMode.Connected);
+				// TODO: The method below is for the most part redundant	
+				
+				// Stops interfering with the highlighting while cropping a picture
+				if (on instanceof Picture) {
+					Picture p = (Picture) on;
+					if (!p.isBeingCropped()) {
+						on = FrameGraphics.Highlight(on.getEditTarget());
+					}
+				} else {
+					on = FrameGraphics.Highlight(on.getEditTarget());
+				}
+				
+			}
+			// if the last item highlighted is still highlighted, clear it
+			if (_lastHoldsHighlight) {
+				_lastHoldsHighlight = false;
+				for (Item i : DisplayController.getCurrentFrame().getItems()) {
+					if (i.isHighlighted() && i != on) {
+						FrameGraphics.changeHighlightMode(i, Item.HighlightMode.None);
+					}
+				}
+			}
+
+		// if the user is not pointing at an item, check for enclosure highlighting
+		} else if (on == null) {
+			Collection<Item> enclosure = FrameUtils.getEnclosingLineEnds();
+			if (enclosure != null && enclosure.size() > 0) {
+				Item firstLineEnd = enclosure.iterator().next();
+				HighlightMode hm;
+				if(StandardInputEventListeners.kbmStateListener.isKeyDown(Key.SHIFT)) {
+					hm = HighlightMode.Connected;
+				} else {
+					hm = HighlightMode.Enclosed;
+				}
+				if (	firstLineEnd.getLines().size() > 1 &&
+						// check that the enclosure is not part of a point being dragged in space
+						!ContainsOneOf(enclosure, FreeItems.getInstance()))
+				{
+					on = firstLineEnd.getLines().get(0);
+					// System.out.println(on == null ? "Null" :
+					// on.toString());
+					FrameGraphics.changeHighlightMode(on, hm);
+				} else if (firstLineEnd instanceof XRayable) {
+					on = firstLineEnd;
+					FrameGraphics.changeHighlightMode(firstLineEnd, hm);
+				}
+				_context = CONTEXT_AT_ENCLOSURE;
+			} else if (_lastHighlightedItem != null) {
+				// System.out.println("LastHighlightedItem");
+				_lastHoldsHighlight = false;
+			}
+		}
+
+		// disable cursor changes when the cursor has items attached
+		if (FreeItems.hasItemsAttachedToCursor() && DisplayController.getCursor() != Item.TEXT_CURSOR) {
+			_forceArrowCursor = false;
+		}
+
+		// setLastHighlightedItem(on);
+
+		if (_lastHighlightedItem != null && _lastHighlightedItem != on && !_lastHoldsHighlight) {
+			// Turn off the highlighting only if the last highlighted item
+			// is not connected to the currentItem. Otherwise we get flickering
+			// in transition from connected to normal mode while moving the cursor along a line.
+			if (on == null || (!on.getAllConnected().contains(_lastHighlightedItem))) {
+				FrameGraphics.changeHighlightMode(_lastHighlightedItem, Item.HighlightMode.None);
+			}
+		}
+
+		_lastHighlightedItem = on;
+
+	}
+
+	private static boolean ContainsOneOf(Collection<Item> enclosure, Collection<Item> freeItems)
+	{
+		if (freeItems == null)
+			return false;
+		for (Item i : freeItems) {
+			if (enclosure.contains(i))
+				return true;
+		}
+		return false;
+	}
+
+	public static void anchor(Item toAnchor, boolean checkEnclosure) {
+		// Only anchor items we have full permission over... i.e. don't anchor vector items
+		if (!toAnchor.hasPermission(UserAppliedPermission.full))
+			return;
+
+		toAnchor.anchor();
+
+		if (checkEnclosure) {
+			ItemUtils.EnclosedCheck(toAnchor.getParentOrCurrentFrame()
+					.getItems());
+			DisplayController.requestRefresh(true);
+		}
+	}
+
+	public static void anchor(Item toAnchor) {
+		anchor(toAnchor, true);
+	}
+
+	public static void anchor(Collection<Item> toAnchor) {
+		boolean bReparse = false;
+		boolean bRecalculate = false;
+		// Need to make sure we check enclosure for overlays etc
+		Set<Frame> checkEnclosure = new HashSet<Frame>();
+
+		// Create a clone of toAnchor since in the proccess of anchoring items
+		// they can change the state of the toAnchor collection and thus create
+		// concurrent modification exceptions.
+		// This is especially needed for widgets being removed when anchored:
+		// since they
+		// currently are composed of 8 items this is vital. In the new revision
+		// of
+		// widgets being implemented as a single item this this can be
+		// depreciated
+		// however it may be useful for other applications.
+		Collection<Item> toAnchorCopy = new ArrayList<Item>(toAnchor);
+
+		for (Item i : toAnchorCopy) {
+			if (toAnchor.contains(i)) { // since to anchor could change while
+				// anchoring
+				// if (!i.hasVector())
+				anchor(i, false);
+				checkEnclosure.add(i.getParentOrCurrentFrame());
+				bReparse |= i.hasOverlay();
+				bRecalculate |= i.recalculateWhenChanged();
+			}
+		}
+
+		toAnchor.clear();
+		// Check enclosure for all the frames of the items that were anchored
+		for (Frame f : checkEnclosure) {
+			// ItemUtils.EnclosedCheck(f.getItems());
+			ItemUtils.Justify(f);
+		}
+
+		Frame currentFrame = DisplayController.getCurrentFrame();
+		if (bReparse)
+			FrameUtils.Parse(currentFrame, false, false);
+		else {
+			currentFrame.notifyObservers(bRecalculate);
+		}
+		DisplayController.requestRefresh(true);
+	}
+	
+	public static void deleteItems(Collection<Item> itemList) {
+		deleteItems(itemList, true);
+	}
+
+	public static void deleteItems(Collection<Item> itemList, boolean addToUndo) {
+		boolean bReparse = false;
+		boolean bRecalculate = false;
+
+		SessionStats.DeletedItems(itemList);
+		List<Frame> modifiedFrames = new LinkedList<Frame>();
+		// Get a list of all the modified frames
+		for (Item i : itemList) {
+			Frame parent = i.getParent();
+			if (parent != null)
+				modifiedFrames.add(parent);
+			i.setHighlightMode(HighlightMode.None);
+			bReparse |= i.hasOverlay();
+			bRecalculate |= i.recalculateWhenChanged();
+		}
+		// If they are all free items then add the current frame
+		if (modifiedFrames.size() == 0) {
+			modifiedFrames.add(DisplayController.getCurrentFrame());
+		}
+
+		Collection<Item> toUndo = new LinkedHashSet<Item>();
+		// disconnect any connected items
+		for (Item i : itemList) {
+
+			// Only delete the current item if have not already deleted.
+			// This is especially important for heavy duty widgets - so they
+			// do not have to expire several times per delete.
+			if (toUndo.contains(i))
+				continue;
+
+			// Make sure text items attached to cursor are reset back to the
+			// transparency they should have.
+			if (i instanceof Text) {
+				((Text) i).setAlpha(-1);
+			}
+
+			if (i.getLines().size() > 0) {
+
+				Collection<Item> toDelete = deleteLineEnd(i);
+				if (addToUndo) {
+					// add the copied items to the undo stack
+					for (Item itemToUndo : toDelete) {
+
+						if (!toUndo.contains(itemToUndo))
+							toUndo.add(itemToUndo);
+
+					}
+				}
+			} else if (!toUndo.contains(i)) {
+				if (addToUndo)
+					toUndo.add(i); // Why was is this a copy
+			}
+		}
+
+		for (Frame f : modifiedFrames) {
+			f.removeAllItems(itemList);
+			// ItemUtils.EnclosedCheck(f.getItems());
+			ItemUtils.Justify(f);
+		}
+		// TODO: How should undelete deal with undo when items are removed from
+		// the current frame as well as the overlay frame
+		Frame currentFrame = DisplayController.getCurrentFrame();
+		currentFrame.addToUndoDelete(itemList);
+		itemList.clear();
+		if (bReparse) {
+			FrameUtils.Parse(currentFrame, false, false);
+			/*
+			 * TODO check if I need to recalculate even if reparse occurs, here
+			 * and in anchor, pickup etc
+			 */
+		} else {
+			currentFrame.notifyObservers(bRecalculate);
+		}
+
+	}
+	
+	private static Collection<Item> deleteLineEnd(Item lineEnd)
+	{
+
+		if (lineEnd instanceof WidgetCorner) { // Brook
+
+			WidgetCorner wc = (WidgetCorner) lineEnd;
+			Frame parent = wc.getWidgetSource().getParentFrame();
+
+			// Remove from the parent frame
+			if (parent != null) {
+				parent.removeAllItems(wc.getWidgetSource().getItems());
+			}
+
+			wc.getWidgetSource().onDelete(); // Changes the widgets
+			// corner/edges ID's...
+
+			return wc.getWidgetSource().getItems();
+
+		} else {
+
+			// // create a backup copy of the dot and its lines
+			// List<Item> copy = copy(lineEnd.getConnected());
+			//			
+			// // Remove lines from their anchored dots
+			// // note: the line is kept so that it can be properly restored
+			// for (Item ic : copy) {
+			// if (ic instanceof Line) {
+			// Line line = (Line) ic;
+			// // Remove the line from the item that is not the copy of the
+			// // line end being deletedF
+			// if (!copy.contains(line.getStartItem()))
+			// line.getStartItem().removeLine(line);
+			// if (!copy.contains(line.getEndItem()))
+			// line.getEndItem().removeLine(line);
+			// }
+			// }
+
+			Collection<Item> copy = lineEnd.getConnected();
+
+			// remove all lines being deleted
+			for (Item ic : lineEnd.getConnected()) {
+				if (ic instanceof Line
+						&& ((Line) ic).getOppositeEnd(lineEnd) != null) {
+					Line line = (Line) ic;
+
+					// Invalidate the line to make sure we dont get any ghost
+					// arrowheads.
+					ic.invalidateAll();
+
+					Item d = line.getOppositeEnd(lineEnd);
+					d.removeLine(line);
+
+					// if the dot was only part of one line, it can be
+					// removed
+					if (d.getLines().size() == 0) {
+						if (d.getParent() != null)
+							d.getParent().removeItem(d);
+						if (!copy.contains(d))
+							copy.add(d);
+					}
+
+					if (lineEnd.getParent() != null)
+						lineEnd.getParent().removeItem(ic);
+				}
+			}
+			return copy;
+		}
+	}
+
+	public static Item getlastHighlightedItem() {
+		return _lastHighlightedItem;
+	}
+	
+	/**
+	 * event called when mouse exits window
+	 * (can't use MouseListener callback since that callback doesn't
+	 *  correctly receive all mouse exit events)
+	 *  *** Above comment no longer applies! ***
+	 *  TODO: Rename. cts16
+	 */
+	public static void mouseExitedWindow()
+	{
+		//System.out.println("Left window");
+		if(FreeItems.hasItemsAttachedToCursor()) {
+			boolean cut = true;
+			for(Item i : FreeItems.getInstance()) {
+				for(Item j : i.getAllConnected()) {
+					if(!FreeItems.getInstance().contains(j)) {
+						cut = false;
+						break;
+					}
+				}
+			}
+			if(cut) {
+				ItemSelection.cut();
+			}
+		}
+	}
+
+	public static void setForceArrow(boolean val) {
+		_forceArrowCursor = val;
+	}
+
+	protected static void zoomFrame(Frame frame, double scaleFactor, int x, int y) {
+
+		if (frame == null) {
+			return;
+		}
+
+		Collection<Item> items = frame.getVisibleItems();
+
+		for (Item item : items) {
+			if (item instanceof Text
+					&& item.getSize() <= Text.MINIMUM_FONT_SIZE
+					&& scaleFactor < 1) {
+				return;
+			}
+		}
+
+		for (Vector v : frame.getVectors()) {
+			v.Source.scale((float) scaleFactor, x, y);
+		}
+
+		for (Item item : items) {
+			// This line is only needed for circles!!
+			// Need to really fix up the way this works!!
+			if (item.hasEnclosures())
+				continue;
+			if (!item.hasPermission(UserAppliedPermission.full))
+				continue;
+			item.invalidateAll();
+			if (!(item instanceof Line)) {
+				item.scale((float) scaleFactor, x, y);
+			}
+		}
+
+		for (Item item : items) {
+			if (!item.hasPermission(UserAppliedPermission.full))
+				continue;
+			// if (!(item instanceof Line))
+			item.invalidateBounds();
+
+			if (item instanceof Line) {
+				((Line) item).refreshStroke(item.getThickness());
+			}
+
+			item.invalidateAll();
+		}
+	}
+
+	public static boolean zoomFrameIfEnabled(Frame frame, double scaleFactor, Point centreOfZoom)
+	{
+		boolean zoom_active = ExperimentalFeatures.FrameZoom.get();
+		
+		if (zoom_active) {
+			
+			zoomFrame(DisplayController.getCurrentFrame(), scaleFactor, centreOfZoom.x, centreOfZoom.y);
+			DisplayController.getCurrentFrame().refreshSize();
+			Refresh();
+		} else {
+			String frameZoomingDisabledMessage = "Frame Zooming currently disabled. "
+					+ "Access Settings->Experimental->FrameZoom and set to 'true' to enable this";
+			MessageBay.displayMessageOnce(frameZoomingDisabledMessage);
+		}
+		
+		return zoom_active;
+	}
+
+	/**
+	 * Performs the dropping action: If the cursor is in free space then: the
+	 * cursor is repositioned below the last non-annotation text item. If the
+	 * cursor is on an item, and has items attached then: the cursor is
+	 * positioned below the pointed to item, and the items below are 'pushed
+	 * down' to make room.
+	 * 
+	 * @param toDropFrom
+	 *            The Item being pointed at by the mouse, may be null to
+	 *            indicate the cursor is in free space.
+	 */
+	public static boolean Drop(Item toDropFrom, boolean bPasting)
+	{
+		try {
+			FrameUtils.setLastEdited(null);
+
+			String newItemText = DEFAULT_NEW_ITEM_TEXT;
+
+			// if a line is being rubber-banded, this is a no-op
+			if (Frame.rubberbandingLine()) return false;
+
+			// if the cursor is in free space then the drop will happen from the
+			// last non annotation text item on the frame
+			if (toDropFrom == null) {
+				toDropFrom = DisplayController.getCurrentFrame().getLastNonAnnotationTextItem();
+			}
+
+			// if no item was found, return
+			if (toDropFrom == null) {
+				MessageBay.errorMessage("No item could be found to drop from");
+				return false;
+			}
+
+			if (!(toDropFrom instanceof Text)) {
+				MessageBay.displayMessage("Only text items can be dropped from");
+				return false;
+			}
+
+			// Get the list of items that must be dropped
+			List<Text> column = DisplayController.getCurrentFrame().getColumn(toDropFrom);
+
+			if (column == null) {
+				MessageBay.errorMessage("No column found to align items to");
+				return false;
+			}
+
+			Item title = DisplayController.getCurrentFrame().getTitleItem();
+
+			// We won't do auto-bulleting when dropping from titles
+			if (!bPasting && toDropFrom != title) {
+				newItemText = getAutoBullet(((Text) toDropFrom).getFirstLine());
+			}
+
+			Text dummyItem = null;
+			if (!bPasting && FreeItems.textOnlyAttachedToCursor()) {
+				dummyItem = (Text) FreeItems.getItemAttachedToCursor();
+				String autoBullet = getAutoBullet(dummyItem.getText());
+
+				if (autoBullet.length() > 0)
+					newItemText = "";
+				dummyItem.setText(newItemText + dummyItem.getText());
+			}
+
+			dummyItem = Text.createText();
+			if (FreeItems.textOnlyAttachedToCursor()) {
+				Text t = (Text) FreeItems.getItemAttachedToCursor();
+				dummyItem.setSize(t.getSize());
+				int lines = t.getTextList().size();
+				for (int i = 0; i < lines; i++) {
+					newItemText += '\n';
+				}
+			}
+
+			dummyItem.setText(newItemText);
+
+			// If the only item on the frame is the title and the frame name
+			// goto the zero frame and drop to the @start if there is one
+			// or a fixed amount if there is not
+			if (column.size() == 0) {
+				Frame current = DisplayController.getCurrentFrame();
+				// Item itemTemplate = current.getItemTemplate();
+				int xPos = title.getX() + FrameCreator.INDENT_FROM_TITLE;
+				int yPos = FrameCreator.getYStart(title);
+				// Check for @start on the zero frame
+				Frame zero = FrameIO.LoadFrame(current.getFramesetName() + '0');
+				Text start = zero.getAnnotation("start");
+				if (start != null) {
+					xPos = start.getX();
+					yPos = start.getY();
+				}
+
+				dummyItem.setPosition(xPos, yPos);
+				// DisplayIO.setCursorPosition(xPos, yPos);
+
+				checkMovingCursor(dummyItem);
+			} else {
+				int yPos = column.get(0).getY() + 1;
+				int xPos = column.get(0).getX();
+				// Either position the new item below the title or just above
+				// the first item below the title
+				if (toDropFrom == title && column.get(0) != title) {
+					// If dropping from the title position just above top item
+					yPos = column.get(0).getY() - 1;
+
+					Frame current = DisplayController.getCurrentFrame();
+					// Check for @start on the zero frame
+					Frame zero = FrameIO
+							.LoadFrame(current.getFramesetName() + '0');
+					Text start = zero.getAnnotation("start");
+					if (start != null) {
+						yPos = Math.min(yPos, start.getY());
+					}
+				}
+				dummyItem.setPosition(xPos, yPos);
+				column.add(dummyItem);
+				FrameUtils.Align(column, false, 0);
+				// Check if it will be outside the frame area
+				if (dummyItem.getY() < 0 || dummyItem.getY() > DisplayController.getFramePaintArea().getHeight())
+				{
+					// Check for the 'next' tag!
+					Frame current = DisplayController.getCurrentFrame();
+					Item next = current.getAnnotation("next");
+					Item prev = current.getAnnotation("previous");
+					// Check for an unlinked next tag
+					if ((next != null && !next.hasLink())
+							|| (prev != null && prev.hasLink())) {
+						Frame firstFrame = current;
+						if (next != null)
+							next.delete();
+						FrameCreator frameCreator = new FrameCreator(null);
+						// Add the next button
+						next = frameCreator.addNextButton(current, null);
+
+						// Create the new frame linked to the next tag
+						boolean mouseMoved = tdfc(next);
+						Frame moreFrame = DisplayController.getCurrentFrame();
+
+						// Add previous button to the new frame
+						frameCreator.addPreviousButton(moreFrame,
+								firstFrame.getName());
+						Item first = current.getAnnotation("first");
+						if (first != null) {
+							frameCreator.addFirstButton(moreFrame,
+									first.getLink());
+						} else {
+							frameCreator.addFirstButton(moreFrame,
+									firstFrame.getName());
+						}
+						// Add the @next if we are pasting
+						// if (bPasting) {
+						// Item copy = next.copy();
+						// copy.setLink(null);
+						// moreFrame.addItem(copy);
+						// }
+
+						moreFrame.setTitle(firstFrame.getTitleItem().getText());
+						// need to move the mouse to the top of the frame if
+						// there wasnt an @start on it
+						if (!mouseMoved) {
+							Item moreTitle = moreFrame.getTitleItem();
+							moreTitle
+									.setOverlayPermission(UserAppliedPermission.full);
+							Drop(moreTitle, bPasting);
+						}
+						// Add the bullet text to the item
+						dummyItem.setPosition(DisplayController.getMouseX(), DisplayController.getMouseY());
+					} else {
+						MessageBay.warningMessage("Can not create items outside the frame area");
+						// ensures correct repainting when items don't move
+						DisplayController.setCursorPosition(DisplayController.getMouseX(), DisplayController.getMouseY());
+						return false;
+					}
+				}
+				if (!FreeItems.textOnlyAttachedToCursor()
+						&& !dummyItem.isEmpty()) {
+					DisplayController.getCurrentFrame().addItem(dummyItem);
+				}
+
+				checkMovingCursor(dummyItem);
+			}
+			if (dummyItem.getText().length() == 0
+					|| FreeItems.hasItemsAttachedToCursor()) {
+				dummyItem.getParentOrCurrentFrame().removeItem(dummyItem);
+				dummyItem.setRightMargin(DisplayController.getFramePaintArea().getWidth(), false);
+			} else {
+				dummyItem.setWidth(toDropFrom.getWidth());
+			}
+
+			DisplayController.resetCursorOffset();
+			DisplayController.requestRefresh(true);
+		} catch (RuntimeException e) {
+			// MessageBay.errorMessage(e.getMessage());
+			e.printStackTrace();
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * @param dummyItem
+	 */
+	public static void moveCursorAndFreeItems(int x, int y)
+	{
+		Point oldMousePosition = DisplayController.getMousePosition();
+
+		if (oldMousePosition.x == x && oldMousePosition.y == y) return;
+
+		DisplayController.setCursorPosition(x, y);
+		Item firstItem = FreeItems.getItemAttachedToCursor();
+
+		if (firstItem == null) return;
+
+		int deltaX = firstItem.getX() - x;
+		int deltaY = firstItem.getY() - y;
+
+		for (Item i : FreeItems.getInstance()) {
+			i.setPosition(i.getX() - deltaX, i.getY() - deltaY);
+		}
+	}
+
+	/**
+	 * @param dummyItem
+	 */
+	private static void checkMovingCursor(Text dummyItem) {
+		// Move the item to the cursor position
+		if (FreeItems.hasItemsAttachedToCursor()) {
+			moveCursorAndFreeItems(dummyItem.getX(), dummyItem.getY());
+		} else {
+			DisplayController.MoveCursorToEndOfItem(dummyItem);
+		}
+	}
+	
+	private static void calculateItem(Item toCalculate)
+	{
+		if (toCalculate == null) return;
+
+		if (!toCalculate.update()) {
+			toCalculate.setFormula(null);
+			MessageBay.errorMessage("Can not calculate formula [" + toCalculate.getText() + ']');
+		}
+	}
+	
+	/**
+	 * Adjusts the size of the given Item, by the given amount. Note: The amount
+	 * is relative and can be positive or negative.
+	 * 
+	 * @param toSet
+	 *            The Item whose size is to be adjusted
+	 * @param diff
+	 *            The amount to adjust the Item's size by
+	 * @param moveCursor
+	 *            true if the cursor position should be automatically adjusted
+	 *            with resizing
+	 */
+	public static void SetSize(Item item, int diff, boolean moveCursor, boolean insideEnclosure, boolean isControlDown)
+	{
+		Collection<Item> toSize = new HashSet<Item>();
+		Collection<Widget> widgets = new HashSet<Widget>();
+		// the mouse is only moved when the Item is on the frame, not free
+		// boolean moveMouse = false;
+		Item toSet = null;
+
+		// if the user is not pointing to any item
+		if (item == null) {
+			if (FreeItems.hasItemsAttachedToCursor())
+				toSize.addAll(FreeItems.getInstance());
+			else {
+				MessageBay.displayMessage("There are no Items selected on the Frame or on the Cursor");
+				return;
+			}
+		} else {
+			if (item.isFrameName()) {
+				// scale the entire frame
+				if (diff != 0) {
+					double scaleFactor = diff > 0 ? 1.1 : 0.909090909;
+					zoomFrameIfEnabled(DisplayController.getCurrentFrame(), scaleFactor, new Point(0, 0));
+				}
+				// MessageBay.displayMessage("Can not resize the frame name");
+				return;
+			}
+			// check permissions
+			if (!item.hasPermission(UserAppliedPermission.full)) {
+				Item editTarget = item.getEditTarget();
+				if (editTarget != item
+						&& editTarget.hasPermission(UserAppliedPermission.full)) {
+					item = editTarget;
+				} else {
+					MessageBay.displayMessage("Insufficient permission to change the size of that item");
+					return;
+				}
+			}
+			toSet = item;
+			// For resizing enclosures pick up everything that is attached to
+			// items partly in the enclosure
+			// TODO make this only pick up stuff COMPLETELY enclosed... if we
+			// change copying to copy only the stuff completely enclosed
+			if (insideEnclosure) {
+				for (Item i : FrameUtils.getCurrentItems(toSet)) {
+					if (i.hasPermission(UserAppliedPermission.full) && !toSize.contains(i)) {
+						toSize.addAll(i.getAllConnected());
+					}
+				}
+
+			}// Enclosed circle centers are resized with the center as origin
+				// Just add the circle center to the list of items to size
+			else if (!toSet.hasEnclosures() && !(toSet instanceof Text)
+					&& toSet.isLineEnd()) {
+				toSize.addAll(toSet.getLines());
+			} else if (toSet instanceof Line) {
+
+				Line line = (Line) toSet;
+
+				if (!(toSet instanceof WidgetEdge) || ((WidgetEdge) toSet).getWidgetSource().isWidgetEdgeThicknessAdjustable())
+				{
+
+					float current = Math.abs(line.getThickness());
+					current = Math.max(current + diff, Item.MINIMUM_THICKNESS);
+					line.setThickness(current);
+					DisplayController.requestRefresh(true);
+					return;
+
+				}
+
+			} else {
+				toSize.add(toSet);
+			}
+		}
+
+		// add widgets to notify
+		for (Item i : toSize) {
+			if (i instanceof WidgetEdge) {
+				widgets.add(((WidgetEdge) i).getWidgetSource());
+			} else if (i instanceof WidgetCorner) {
+				widgets.add(((WidgetCorner) i).getWidgetSource());
+			}
+		}
+
+		Point origin = EcosystemManager.getInputManager().getCursorPosition();
+		// Inside enclosures increase the size of the enclosure
+		double ratio = (100.0 + diff * 2) / 100.0;
+		if (insideEnclosure) {
+			Collection<Item> done = new HashSet<Item>();
+			// adjust the size of all the items
+			for (Item i : toSize) {
+				if (done.contains(i)) continue;
+
+				if (i.isLineEnd()) {
+					if (!(i instanceof WidgetCorner) || !((WidgetCorner) i).getWidgetSource().isFixedSize()) // don't size fixed widgets
+					{
+						Collection<Item> allConnected = i.getAllConnected();
+						done.addAll(allConnected);
+						for (Item it : allConnected) {
+							it.translate(origin, ratio);
+							it.setArrowheadLength((float) (it.getArrowheadLength() * ratio));
+						}
+						i.setThickness((float) (i.getThickness() * ratio));
+					}
+				} else if (i instanceof XRayable) {
+					XRayable xRay = (XRayable) i;
+					Text source = xRay.getSource();
+					// Ensure that the source is done before the XRayable
+					if (!done.contains(source)) {
+						scaleText(insideEnclosure, origin, ratio, done, source);
+					}
+					i.translate(origin, ratio);
+					i.setThickness((float) (i.getThickness() * ratio));
+					done.add(i);
+				} else if (i.hasVector()) {
+					// TODO Improve the effiency of resizing vectors... ie...
+					// dont want to have to reparse all the time
+					assert (i instanceof Text);
+					Text text = (Text) i;
+					AttributeValuePair avp = new AttributeValuePair(text.getText());
+					double scale = 1F;
+					try {
+						scale = avp.getDoubleValue();
+					} catch (Exception e) {
+					}
+					scale *= ratio;
+					NumberFormat nf = Vector.getNumberFormatter();
+					text.setAttributeValue(nf.format(scale));
+					text.translate(origin, ratio);
+					item.getParent().parse();
+				} else if (i instanceof Text) {
+					scaleText(insideEnclosure, origin, ratio, done, (Text) i);
+				}
+			}
+			// refresh anchored items
+			if (refreshAnchors(toSize)) {
+				FrameUtils.Parse(DisplayController.getCurrentFrame(), false);
+			}
+			// notify widgets they were resized
+/*			for (InteractiveWidget iw : widgets) {
+				iw.onResized();
+			}*/
+			DisplayController.requestRefresh(true);
+			return;
+		}
+
+		// adjust the size of all the items
+		for (Item i : toSize) {
+			// Lines and dots use thickness, not size
+			if (i.hasEnclosures()) {
+				Circle c = (Circle) i.getEnclosures().iterator().next();
+				c.setSize(c.getSize() * (float) ratio);
+			} else if (i instanceof Line || i instanceof Circle && !insideEnclosure) {
+				float current = Math.abs(i.getThickness());
+				current = Math.max(current + diff, Item.MINIMUM_THICKNESS);
+				i.setThickness(current);
+			} else if (i instanceof Dot) {
+				Item dot = (Item) i;
+				float current = Math.abs(dot.getThickness());
+				current = Math.max(current + diff, Item.MINIMUM_THICKNESS);
+				dot.setThickness(current);
+			} else if (i.hasVector()) {
+				assert (item instanceof Text);
+				Text text = (Text) item;
+				AttributeValuePair avp = new AttributeValuePair(text.getText());
+				double scale = 1F;
+				try {
+					scale = avp.getDoubleValue();
+				} catch (Exception e) {
+				}
+				scale *= ratio;
+				NumberFormat nf = Vector.getNumberFormatter();
+				text.setAttributeValue(nf.format(scale));
+				text.translate(origin, ratio);
+				item.getParent().parse();
+			} else {
+				float oldSize = Math.abs(i.getSize());
+				float newSize = Math.max(oldSize + diff, Item.MINIMUM_THICKNESS);
+				float resizeRatio = newSize / oldSize;
+				// Set size for Picture also translates
+				i.setSize(newSize);
+				if (i instanceof Text && i.getSize() != oldSize) {
+					if (toSize.size() == 1 && !isControlDown) {
+						moveCursorAndFreeItems(i.getX(), i.getY());
+					} else {
+						i.translate(origin, resizeRatio);
+						if (i.isLineEnd()) {
+							i.setPosition(i.getPosition());
+						}
+					}
+				}
+			}
+		}
+
+		if (toSet != null)
+			toSet.getParent().setChanged(true);
+
+		// refresh anchored items
+		if (refreshAnchors(toSize)) {
+			FrameUtils.Parse(DisplayController.getCurrentFrame(), false);
+		}
+		
+		DisplayController.requestRefresh(true);
+	}
+
+	/**
+	 * @param origin
+	 * @param ratio
+	 * @param done
+	 * @param source
+	 */
+	private static void scaleText(boolean insideEnclosure, Point origin, double ratio, Collection<Item> done, Text source)
+	{
+		if (insideEnclosure) source.setWidth(Math.round((float) (source.getAbsoluteWidth() * ratio)));
+		source.translate(origin, ratio);
+		source.setSize((float) (source.getSize() * ratio));
+		done.add(source);
+	}
+
+	private static boolean refreshAnchors(Collection<Item> items) {
+		boolean bReparse = false;
+
+		for (Item i : items) {
+			Integer anchorLeft = i.getAnchorLeft();
+			Integer anchorRight = i.getAnchorRight();
+			Integer anchorTop = i.getAnchorTop();
+			Integer anchorBottom = i.getAnchorBottom();
+
+			if (anchorLeft != null) {
+				i.setAnchorLeft(anchorLeft);
+				if (i.hasVector()) {
+					bReparse = true;
+				}
+			}
+
+			if (anchorRight != null) {
+				i.setAnchorRight(anchorRight);
+				if (i.hasVector()) {
+					bReparse = true;
+				}
+			}
+
+			if (anchorTop != null) {
+				i.setAnchorTop(anchorTop);
+				if (i.hasVector()) {
+					bReparse = true;
+				}
+			}
+
+			if (anchorBottom != null) {
+				i.setAnchorBottom(anchorBottom);
+				if (i.hasVector()) {
+					bReparse = true;
+				}
+			}
+		}
+		return bReparse;
+	}
+
+	/**
+	 * Sets the colour of the current Item based on its current colour. The
+	 * colours proceed in the order stored in COLOR_WHEEL.
+	 * 
+	 * @param toSet
+	 *            The Item whose colour is to be changed
+	 */
+	private static void SetColor(Item item, boolean setTransparent, boolean setBackgroundColor)
+	{
+		// first determine the next color
+		Colour color = null;
+		Frame currentFrame = DisplayController.getCurrentFrame();
+		if (item == null) {
+			if (FreeItems.hasItemsAttachedToCursor()) {
+				color = FreeItems.getInstance().get(0).getColor();
+			} else {
+				return;
+			}
+		// change the background color if the user is pointing on the frame name
+		} else if (item == currentFrame.getNameItem()) {
+			// check permissions
+			if (!item.hasPermission(UserAppliedPermission.full)) {
+				MessageBay.displayMessage("Insufficient permission to the frame's background color");
+				return;
+			}
+			
+			if (setTransparent) {
+				currentFrame.setBackgroundColor(null);
+			} else {
+				currentFrame.toggleBackgroundColor();
+			}
+
+			DisplayController.requestRefresh(true);
+			return;
+		} else {
+			// check permissions
+			if (!item.hasPermission(UserAppliedPermission.full)) {
+				Item editTarget = item.getEditTarget();
+				if (editTarget != item && editTarget.hasPermission(UserAppliedPermission.full)) {
+					item = editTarget;
+				} else {
+					MessageBay.displayMessage("Insufficient permission to change color");
+					return;
+				}
+			}
+			
+			// Toggling color of circle center changes the circle fill color
+			if (item.hasEnclosures()) {
+				if (setBackgroundColor) {
+					SetGradientColor(item.getEnclosures().iterator().next(), setTransparent);
+				} else {
+					SetFillColor(item.getEnclosures().iterator().next(), setTransparent);
+				}
+			} else if (setBackgroundColor) {
+				color = item.getPaintBackgroundColor();
+			} else {
+				color = item.getPaintColor();
+			}
+		}
+		
+		if (setTransparent) {
+			color = null;
+		} else if (setBackgroundColor) {
+			color = ColorUtils.getNextColor(color, TemplateSettings.FillColorWheel.get(), item.getPaintColor());
+		} else {
+			color = ColorUtils.getNextColor(color, TemplateSettings.ColorWheel.get(), currentFrame.getPaintBackgroundColor());
+		}
+
+		if (setBackgroundColor) {
+			if (item == null && FreeItems.hasItemsAttachedToCursor()) {
+				for (Item i : FreeItems.getInstance()) {
+					i.setBackgroundColor(color);
+				}
+			} else {
+				item.setBackgroundColor(color);
+				item.getParent().setChanged(true);
+			}
+		} else {
+			if (item == null && FreeItems.hasItemsAttachedToCursor()) {
+				for (Item i : FreeItems.getInstance()) {
+					i.setColor(color);
+				}
+			} else {
+				item.setColor(color);
+				item.getParent().setChanged(true);
+			}
+		}
+		
+		DisplayController.requestRefresh(true);
+	}
+
+	private static void SetFillColor(Item item, boolean setTransparent)
+	{
+		if (item == null) return;
+
+		if (!item.hasPermission(UserAppliedPermission.full)) {
+			MessageBay.displayMessage("Insufficient permission to change fill color");
+			return;
+		}
+
+		Item toSet = item;
+		Colour color = toSet.getFillColor();
+		if (setTransparent) {
+			color = null;
+		} else {
+			color = ColorUtils.getNextColor(color, TemplateSettings.FillColorWheel.get(), toSet.getGradientColor());
+		}
+		
+		// if (color == null) {
+		// MessageBay.displayMessage("FillColor is now transparent");
+		// }
+
+		toSet.setFillColor(color);
+		toSet.getParent().setChanged(true);
+
+		DisplayController.requestRefresh(true);
+	}
+
+	private static void SetGradientColor(Item item, boolean setTransparent)
+	{
+		if (item == null) return;
+
+		if (!item.hasPermission(UserAppliedPermission.full)) {
+			MessageBay.displayMessage("Insufficient permission to change gradient color");
+			return;
+		}
+
+		Item toSet = item;
+		Colour color = toSet.getGradientColor();
+		if (setTransparent) {
+			color = null;
+		} else {
+			color = ColorUtils.getNextColor(color, TemplateSettings.ColorWheel.get(), toSet.getFillColor());
+		}
+
+		// if (color == null) {
+		// MessageBay.displayMessage("FillColor is now transparent");
+		// }
+
+		toSet.setGradientColor(color);
+		toSet.getParent().setChanged(true);
+
+		DisplayController.requestRefresh(true);
+	}
+
+	/**
+	 * Toggles the given Item's annotation status on\off.
+	 * 
+	 * @param toToggle
+	 *            The Item to toggle
+	 */
+	private static void ToggleAnnotation(Item toToggle) {
+		if (toToggle == null) {
+			MessageBay.displayMessage("There is no Item selected to toggle");
+			return;
+		}
+
+		// check permissions
+		if (!toToggle.hasPermission(UserAppliedPermission.full)) {
+			MessageBay.displayMessage("Insufficient permission to toggle that item's annotation");
+			return;
+		}
+		toToggle.setAnnotation(!toToggle.isAnnotation());
+
+		toToggle.getParent().setChanged(true);
+		DisplayController.requestRefresh(true);
+	}
+
+	/**
+	 * Toggles the face style of a text item
+	 * 
+	 * @param toToggle
+	 *            The Item to toggle
+	 */
+	private static void ToggleFontStyle(Item toToggle) {
+		if (toToggle == null) {
+			MessageBay.displayMessage("There is no Item selected to toggle");
+			return;
+		}
+
+		// check permissions
+		if (!toToggle.hasPermission(UserAppliedPermission.full)) {
+			MessageBay.displayMessage("Insufficient permission to toggle that item's annotation");
+			return;
+		}
+
+		if (toToggle instanceof Text) {
+			Text text = (Text) toToggle;
+			text.toggleFontStyle();
+
+			text.getParent().setChanged(true);
+			DisplayController.requestRefresh(true);
+		}
+	}
+
+	/**
+	 * Toggles the face style of a text item
+	 * 
+	 * @param toToggle
+	 *            The Item to toggle
+	 */
+	private static void ToggleFontFamily(Item toToggle) {
+		if (toToggle == null) {
+			MessageBay.displayMessage("There is no Item selected to toggle");
+			return;
+		}
+
+		// check permissions
+		if (!toToggle.hasPermission(UserAppliedPermission.full)) {
+			MessageBay.displayMessage("Insufficient permission to toggle that item's annotation");
+			return;
+		}
+
+		if (toToggle instanceof Text) {
+			Text text = (Text) toToggle;
+			text.toggleFontFamily();
+
+			text.getParent().setChanged(true);
+			DisplayController.requestRefresh(true);
+		}
+	}
+
+	/**
+	 * Creates a new Frameset with the name given by the Item
+	 * 
+	 * @param name
+	 */
+	private static void CreateFrameset(Item item)
+	{
+		if (item == null) {
+			MessageBay.displayMessage("There is no selected item to use for the frameset name");
+			return;
+		}
+
+		if (!(item instanceof Text)) {
+			MessageBay.displayMessage("Framesets can only be created from text items");
+			return;
+		}
+
+		// Don't create frameset if the item is linked
+		if (item.getLink() != null) {
+			MessageBay.displayMessage("A frameset can not be created from a linked item");
+			return;
+		}
+
+		// check permissions
+		if (!item.hasPermission(UserAppliedPermission.full)) {
+			MessageBay.displayMessage("Insufficient permission to create a frameset from this item");
+			return;
+		}
+
+		Text text = (Text) item;
+		try {
+			// create the new frameset
+			Frame linkTo = FrameIO.CreateNewFrameset(text.getFirstLine());
+			DisplayController.setCursor(Item.DEFAULT_CURSOR);
+			text.setLink(linkTo.getName());
+			text.getParent().setChanged(true);
+			FrameUtils.DisplayFrame(linkTo, true, true);
+			linkTo.moveMouseToDefaultLocation();
+			// this needs to be done if the user doesn't move the mouse before
+			// doing Tdfc while the cursor is set to the text cursor
+			DisplayController.setCursor(Item.DEFAULT_CURSOR);
+		} catch (Exception e) {
+			MessageBay.errorMessage(e.getMessage());
+		}
+	}
+
+	private void move(int direction, boolean isShiftDown, boolean isCtrlDown) {
+		Item on = FrameUtils.getCurrentItem();
+
+		if ((on == null) || (on instanceof Picture)){
+			navigateFrame(direction);
+			return;
+		}
+
+		if (on instanceof Text) {
+			Text text = (Text) on;
+			// When the user hits the left and right button with mouse
+			// positions over the the frame name navigation occurs
+			if (text.isFrameName()) {
+				navigateFrame(direction);
+				return;
+			} else {
+				FrameUtils.setLastEdited(text);
+				DisplayController.setTextCursor(text, direction, false, isShiftDown, isCtrlDown, true);
+			}
+		}
+	}
+
+	private void navigateFrame(int direction) {
+		switch (direction) {
+		case Text.RIGHT:
+		case Text.PAGE_UP:
+			Navigation.NextFrame(false);
+			break;
+		case Text.LEFT:
+		case Text.PAGE_DOWN:
+			Navigation.PreviousFrame(false);
+			break;
+		case Text.HOME:
+		case Text.LINE_HOME:
+			Navigation.ZeroFrame();
+			break;
+		case Text.END:
+		case Text.LINE_END:
+			Navigation.LastFrame();
+			break;
+		}
+	}
+
+	/**
+	 * Moves the cursor to the next text item on the frame
+	 * 
+	 * @param currentItem
+	 * @param direction
+	 *            move up if direction is negative, down if direction is
+	 *            positive
+	 */
+	public static void NextTextItem(Item currentItem, boolean down) {
+		// Move the cursor to the next text item
+		Frame current = DisplayController.getCurrentFrame();
+		Text title = current.getTitleItem();
+
+		Collection<Text> currentItems = FrameUtils.getCurrentTextItems();
+		List<Text> textItems = new ArrayList<Text>();
+		// Move to the next text item in the box if
+		if (currentItems.contains(currentItem)) {
+			textItems.addAll(currentItems);
+		} else {
+			if (title != null)
+				textItems.add(title);
+			textItems.addAll(current.getBodyTextItems(true));
+		}
+
+		Collections.sort(textItems);
+
+		if (textItems.size() == 0) {
+			// If there are no text items on the frame its a NoOp
+			if (title == null)
+				return;
+			if (title != null)
+				DisplayController.MoveCursorToEndOfItem(title);
+			DisplayController.requestRefresh(true);
+			return;
+		}
+
+		// If the user is mouse wheeling in free space...
+		if (currentItem == null) {
+			// find the nearest item in the correct direction
+			int currY = DisplayController.getMouseY();
+			for (int i = 0; i < textItems.size(); i++) {
+				Item t = textItems.get(i);
+				if (currY < t.getY()) {
+					if (down) {
+						DisplayController.MoveCursorToEndOfItem(t);
+					} else {
+						if (i == 0) {
+							DisplayController.MoveCursorToEndOfItem(current
+									.getTitleItem());
+						} else {
+							DisplayController.MoveCursorToEndOfItem(textItems
+									.get(i - 1));
+						}
+					}
+					DisplayController.requestRefresh(true);
+					return;
+				}
+			}
+			// If we are at the bottom of the screen and the user scrolls down
+			// then scroll backup to the title
+			if (textItems.size() > 0) {
+				DisplayController.MoveCursorToEndOfItem(textItems.get(textItems.size() - 1));
+			}
+			return;
+		}
+
+		// Find the current item... then move to the next item
+		int i = textItems.indexOf(currentItem);
+
+		int nextIndex = i + (down ? 1 : -1);
+		if (nextIndex >= 0 && nextIndex < textItems.size()) {
+			DisplayController.MoveCursorToEndOfItem(textItems.get(nextIndex));
+		} else {
+			DisplayController.MoveCursorToEndOfItem(currentItem);
+		}
+		return;
+	}
+
+	private static void copyItemToClipboard(Item on)
+	{
+		if (on == null || !(on instanceof Text)) return;
+
+		Text text = (Text) on;
+		String string = text.copySelectedText();
+
+		if (string == null || string.length() == 0) string = text.getText();
+
+		// add the text of the item to the clipboard
+		ClipboardData clipboardData = ClipboardData.fromString(string);
+		EcosystemManager.getClipboardManager().set(clipboardData);
+	}
+
+	public static void delete(Item toDelete, Collection<Item> deleteItems, Collection<Item> deleteEnclosure, boolean alternateMode)
+	{
+		boolean bRecalculate = false;
+
+		FrameUtils.setLastEdited(null);
+		_offX = _offY = 0;
+
+		Frame currentFrame = DisplayController.getCurrentFrame();
+		// check if the user is pointing at the frame's framename
+		if (toDelete != null && toDelete == currentFrame.getNameItem()) {
+			currentFrame.clear(false);
+			DisplayController.requestRefresh(true);
+			return;
+		}
+
+		// if the user is deleting items attached to the cursor
+		if (FreeItems.hasItemsAttachedToCursor()) {
+			// if this is an item-swap
+			if (FreeItems.getInstance().size() == 1
+					&& FreeItems.getInstance().get(0) instanceof Text
+					&& toDelete != null && toDelete instanceof Text) {
+
+				// check permissions
+				if (!toDelete.hasPermission(UserAppliedPermission.full)) {
+					MessageBay.displayMessage("Insufficient permission to swap Item text");
+					return;
+				}
+				Text anchored = (Text) toDelete;
+				Text free = (Text) FreeItems.getInstance().get(0);
+				SessionStats.DeletedItem(free);
+				// List<String> temp = anchored.getText();
+				anchored.setText(free.getText());
+				anchored.setFormula(free.getFormula());
+
+				// free.setTextList(temp);
+				FreeItems.getInstance().clear();
+
+				anchored.getParent().setChanged(true);
+
+				bRecalculate |= free.recalculateWhenChanged();
+				bRecalculate |= anchored.recalculateWhenChanged();
+
+				// update the offset since the text has changed
+				_offX = DisplayController.getMouseX() - anchored.getX() + anchored.getOffset().x;
+				_offY = DisplayController.getMouseY() - anchored.getY() + anchored.getOffset().y;
+			} else {
+				// if shift is pressed delete the entire shape attached to the dot
+				if(alternateMode) {
+    				List<Item> tmp = new ArrayList<Item>(FreeItems.getInstance());
+    				for(Item i : tmp) {
+    					// remove entire rectangles instead of just the corner
+    					if(i instanceof Dot) {
+    						FreeItems.getInstance().addAll(i.getAllConnected());
+    						for(Item j : i.getAllConnected()) {
+    							if(j instanceof Dot) {
+    								FreeItems.getInstance().addAll(j.getLines());
+    							}
+    						}
+    					}
+    				}
+				}
+				deleteItems(FreeItems.getInstance());
+			}
+			// reset the mouse cursor
+			updateCursor();
+			// the user is not pointing at an item
+		} else if (toDelete == null) {
+		
+			// if the user is pointing inside a closed shape, delete it
+			
+			Collection<Item> items = null;
+			// if shift is down, only delete the enclosing shape (ignore the items inside)
+			if(alternateMode) {
+				Collection<Item> tmp = deleteEnclosure;
+				if(tmp != null) {
+					items = new ArrayList<Item>();
+					items.addAll(tmp);
+    				for(Item i : tmp) {
+    					if(i instanceof Dot) {
+    						items.addAll(((Dot)i).getLines());
+    					}
+    				}
+				}
+			} else {
+				items = deleteItems;
+			}
+			
+			if (items != null) {
+				Collection<Item> toRemove = new LinkedHashSet<Item>(items.size());
+				for (Item ip : items) {
+					if (ip.hasPermission(UserAppliedPermission.full)) {
+						// Only include lines if one of their enpoints are also
+						// being removed
+						if (ip instanceof Line) {
+							Line l = (Line) ip;
+							Item end = l.getEndItem();
+							Item start = l.getStartItem();
+
+							// If one end of a line is being deleted, remove the
+							// other end if all its connecting lines are being
+							// deleted
+							if (items.contains(end)) {
+								if (!items.contains(start) && items.containsAll(start.getLines())) {
+									toRemove.add(start);
+								}
+							} else if (items.contains(start)) {
+								if (items.containsAll(end.getLines())) {
+									toRemove.add(end);
+								}
+							} else {
+								continue;
+							}
+						}
+						toRemove.add(ip);
+					}
+				}
+
+				deleteItems(toRemove);
+
+				// reset the mouse cursor
+				updateCursor();
+				DisplayController.requestRefresh(true);
+
+				// otherwise this is an undo command
+			}
+			return;
+			// this is a delete command
+		} else {
+			
+			// Special case if toDelete item is an image: only want to delete if over non-background pixel color
+			if (toDelete instanceof Picture) {
+				int mouseX = DisplayController.getMouseX();
+				int mouseY = DisplayController.getMouseY();
+				Picture clickedOnPicture = (Picture)toDelete;
+				Frame current_frame = DisplayController.getCurrentFrame();
+				Colour bg_col = current_frame.getBackgroundColor();
+				if (clickedOnPicture.MouseOverBackgroundPixel(mouseX,mouseY,bg_col)) {
+					return;
+				}
+				// If get to here, then user clicked on an image (non-trival pixel color),
+				// so go ahead and let it be deleted in the usual way
+			}
+				
+			
+			// check permissions
+			if (!toDelete.hasPermission(UserAppliedPermission.full)) {
+				Item editTarget = toDelete.getEditTarget();
+				if (editTarget != toDelete
+						&& editTarget.hasPermission(UserAppliedPermission.full)) {
+					toDelete = editTarget;
+				} else {
+					MessageBay.displayMessage("Insufficient permission to delete item");
+					return;
+				}
+			}
+
+			Frame parent = toDelete.getParent();
+			if (parent != null) {
+				parent.setChanged(true);
+			}
+			Collection<Item> toUndo = null;
+			if (toDelete.isLineEnd()) {
+				// delete the entire connected shape if shift is down
+				if(alternateMode) {
+					List<Item> tmp = new ArrayList<Item>();
+					tmp.add(toDelete);
+					// remove entire shape instead of just the corner
+					tmp.addAll(toDelete.getAllConnected());
+					for(Item j : toDelete.getAllConnected()) {
+						if(j instanceof Dot) {
+							tmp.addAll(j.getLines());
+						}
+					}
+					deleteItems(tmp);
+					return;
+				} else {
+					toUndo = deleteLineEnd(toDelete);
+				}
+				// delete the entire connected shape if shift is down, unless we're hovering the end of the line
+			} else if (toDelete instanceof WidgetEdge) { // must notify
+				// widgets that they
+				// are being deleted
+				((WidgetEdge) toDelete).getWidgetSource().onDelete();
+				toUndo = toDelete.getConnected();
+			} else if (toDelete instanceof Line && alternateMode || toDelete.getHighlightMode() == Item.HighlightMode.Disconnect) {
+				Line line = (Line) toDelete;
+				Item start = line.getStartItem();
+				Item end = line.getEndItem();
+				Collection<Item> delete = new LinkedList<Item>();
+				delete.add(toDelete);
+				if (end.getLines().size() == 1) {
+					delete.add(end);
+				} else {
+					end.removeLine(line);
+				}
+				if (start.getLines().size() == 1) {
+					delete.add(start);
+				} else {
+					start.removeLine(line);
+				}
+				toUndo = delete;
+			} else {
+				bRecalculate |= toDelete.recalculateWhenChanged();
+				toUndo = toDelete.getConnected(); // copy(toDelete.getConnected());
+			}
+			SessionStats.DeletedItems(toUndo);
+			if (parent != null) {
+				parent.addToUndoDelete(toUndo);
+				parent.removeAllItems(toUndo); // toDelete.getConnected()
+			}
+			// reset the mouse cursor
+			updateCursor();
+			if (parent != null)
+				// ItemUtils.EnclosedCheck(parent.getItems());
+				ItemUtils.Justify(parent);
+			if (toDelete.hasOverlay()) {
+				FrameUtils.Parse(parent, false, false);
+				DisplayController.requestRefresh(false);
+			}
+
+			DisplayController.setCursor(Item.DEFAULT_CURSOR);
+
+		}
+
+		currentFrame.notifyObservers(bRecalculate);
+
+		DisplayController.requestRefresh(true);
+	}
+	
+	public static void mouseEnteredWindow()
+	{
+		// if there is expeditee data on the clipboard that has not yet been autoPasted, autoPaste it
+		//Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
+		//Transferable content = c.getContents(null);
+		ClipboardData clipboardData = EcosystemManager.getClipboardManager().get();
+		try {
+			if (clipboardData.data instanceof ExpDataHandler) {	// Expeditee data
+				ExpDataHandler expData = (ExpDataHandler) clipboardData.data;
+				if(expData.autoPaste) {
+    				List<Item> items = new ExpClipReader(DisplayController.getMouseX(), DisplayController.getMouseY()).read(expData.items);
+   					// generate new IDs and pickup
+   					pickup(ItemUtils.CopyItems(items));
+   					// update the clipboard contents so they won't be autoPasted again
+   					expData.autoPaste = false;
+/*       				String stringData = "";
+       					Image imageData = null;
+       					if(content.isDataFlavorSupported(DataFlavor.stringFlavor)) {
+       						stringData = (String) content.getTransferData(DataFlavor.stringFlavor);
+       					}
+       					if(content.isDataFlavorSupported(DataFlavor.imageFlavor)) {
+       						imageData = (Image) content.getTransferData(DataFlavor.imageFlavor);
+       					}
+          				c.setContents(new ItemSelection(stringData, imageData, expData), null);
+*/
+   					EcosystemManager.getClipboardManager().set(clipboardData);
+				}
+			}
+		} catch(Exception ex) {
+			ex.printStackTrace(); 
+		}
+	}
+	
+	private static boolean oldControlDown = false;
+	
+	private void mouseMoved(Point to)
+	{
+
+		boolean shiftStateChanged = false;
+		if (oldControlDown != StandardInputEventListeners.kbmStateListener.isKeyDown(Key.CTRL)) {
+			shiftStateChanged = true;
+			oldControlDown = StandardInputEventListeners.kbmStateListener.isKeyDown(Key.CTRL);
+		}
+		
+		float MouseX = to.getX();
+		float MouseY = to.getY();
+
+		// Moving the mouse a certain distance removes the last edited text if it is empty
+		Text lastEdited = FrameUtils.getLastEdited();
+		if (	lastEdited != null &&
+				lastEdited.getText().length() == 0 &&
+				lastEdited.getPosition().getDistanceTo(to) > 20) // TODO: Remove magic constant. cts16
+		{
+			FrameUtils.setLastEdited(null);
+		}
+
+		// If shift is down then the movement is constrained
+		if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.CTRL) && FreeItems.getInstance().size() > 0) {
+			// Check if we are rubber banding a line
+			if (shiftStateChanged && FreeItems.rubberBanding()) {
+				// Get the line end that is being rubber banded
+				Item thisEnd = FreeItems.getInstance().get(0).isLineEnd() ?
+									FreeItems.getInstance().get(0) : FreeItems.getInstance().get(1);
+				Line line = (Line) (FreeItems.getInstance().get(0).isLineEnd() ?
+										FreeItems.getInstance().get(1) : FreeItems.getInstance().get(0));
+				Item otherEnd = line.getOppositeEnd(thisEnd);
+				int deltaX = Math.abs(to.getX() - otherEnd.getX());
+				int deltaY = Math.abs(to.getY() - otherEnd.getY());
+				// Check if its a vertical line
+				if (deltaX < deltaY / 2) {
+					new Constraint(thisEnd, otherEnd, thisEnd.getParentOrCurrentFrame().getNextItemID(), Constraint.VERTICAL);
+				}
+				// Check if its horizontal
+				else if (deltaY <= deltaX / 2) {
+					new Constraint(thisEnd, otherEnd, thisEnd.getParentOrCurrentFrame().getNextItemID(), Constraint.HORIZONTAL);
+				} else {
+					// Add DIAGONAL constraints
+					int constraint = Constraint.DIAGONAL_NEG;
+					// Check if the slope is positive
+					if ((thisEnd.getY() - otherEnd.getY()) / (double) (thisEnd.getX() - otherEnd.getX()) > 0.0) {
+						constraint = Constraint.DIAGONAL_POS;
+					}
+
+					new Constraint(thisEnd, otherEnd, thisEnd.getParentOrCurrentFrame().getNextItemID(), constraint);
+				}
+			// If it's a line-end attached to two lines lengthen the
+			// shorter so it is the same length as the longer line
+			} else if (FreeItems.getInstance().size() == 3) {
+				// check if we are rubber banding the corner of a shape
+				Item thisEnd = getShapeCorner(FreeItems.getInstance());
+				if (thisEnd != null) {
+					Line line1 = thisEnd.getLines().get(0);
+					Line line2 = thisEnd.getLines().get(1);
+					
+					// Check if the two lines are constrained and hence it is a rectangle
+					Integer c1 = line1.getPossibleConstraint();
+					Integer c2 = line2.getPossibleConstraint();
+
+					if (c1 != null && c2 != null) {
+						// This is the case of a constrained rectangle
+						if (	(c1 == Constraint.VERTICAL && c2 == Constraint.HORIZONTAL) ||
+								(c1 == Constraint.HORIZONTAL && c2 == Constraint.VERTICAL))
+						{
+							Line vLine = line2;
+							Line hLine = line1;
+							if (c1 == Constraint.VERTICAL) {
+								vLine = line1;
+								hLine = line2;
+							}
+							Item hOtherEnd = hLine.getOppositeEnd(thisEnd);
+							Item vOtherEnd = vLine.getOppositeEnd(thisEnd);
+
+							double vLength = Math.abs(vOtherEnd.getY() - MouseY);
+							double hLength = Math.abs(hOtherEnd.getX() - MouseX);
+
+							if (vLength > hLength) {
+								MouseX = Math.round(hOtherEnd.getX() + vLength * (MouseX > hOtherEnd.getX() ? 1 : -1));
+							} else /* if (hLength > vLength) */{
+								MouseY = Math.round(vOtherEnd.getY() + hLength * (MouseY > vOtherEnd.getY() ? 1 : -1));
+							}
+						}
+					// Other wise it is a not constrained shape so constrain
+					// the two lines lengths to be equal
+					} else {
+						Item lineEnd1 = line1.getOppositeEnd(thisEnd);
+						Item lineEnd2 = line2.getOppositeEnd(thisEnd);
+						double l1 = Point.distanceBetween(lineEnd1.getPosition(), to);
+						double l2 = Point.distanceBetween(lineEnd2.getPosition(), to);
+						double l3 = Point.distanceBetween(lineEnd1.getPosition(), lineEnd2.getPosition());
+						// l1 needs to be the shorter end
+						if (l1 > l2) {
+							Item temp = lineEnd1;
+							lineEnd1 = lineEnd2;
+							lineEnd2 = temp;
+							double tempL = l1;
+							l1 = l2;
+							l2 = tempL;
+						}
+						// Now use the cosine rule to calculate the angle between l1 and l3
+						double cosTheta = (l1 * l1 + l3 * l3 - l2 * l2)
+								/ (2 * l1 * l3);
+						// now calculate the new length for the lines using cos rule
+						double l_new = l3 / (2 * cosTheta);
+						double ratio = l_new / l1;
+						MouseX = Math.round((to.getX() - lineEnd1.getX()) * ratio) + lineEnd1.getX();
+						MouseY = Math.round((to.getY() - lineEnd1.getY()) * ratio) + lineEnd1.getY();
+					}
+				}
+			}
+		} else if (shiftStateChanged && !StandardInputEventListeners.kbmStateListener.isKeyDown(Key.CTRL) && FreeItems.rubberBanding()) {
+			// Get the line end that is being rubber banded
+			Item thisEnd = FreeItems.getInstance().get(0).isLineEnd() ?
+								FreeItems.getInstance().get(0) : FreeItems.getInstance().get(1);
+			thisEnd.removeAllConstraints();
+		}
+
+		refreshHighlights();
+
+		if (FreeItems.hasCursor()) {
+			move(FreeItems.getCursor(), to, true);
+		}
+
+		if (FreeItems.hasItemsAttachedToCursor()) {
+			move(FreeItems.getInstance(), to);
+		}
+
+		if (_forceArrowCursor) updateCursor();
+
+		_forceArrowCursor = true;
+	}
+
+	/**
+	 * Gets the rectangle corner from the list of items that are part of a
+	 * rectangle.
+	 * 
+	 * @param partialRectangle
+	 *            a corner and its two connecting lines.
+	 * @return the rectangle corner or null if the list of items is not part of
+	 *         a rectangle.
+	 */
+	private static Item getShapeCorner(List<Item> partialRectangle) {
+		if (partialRectangle.size() < 3)
+			return null;
+		Item lineEnd = null;
+		// only one lineEnd will be present for rectangles
+		// All other items must be lines
+		for (Item i : partialRectangle) {
+			if (i.isLineEnd()) {
+				if (lineEnd == null) {
+					lineEnd = i;
+				} else {
+					return null;
+				}
+			} else if (!(i instanceof Line)) {
+				return null;
+			}
+		}
+		// if this is at least the corner of two connected lines
+		if (lineEnd != null && lineEnd.getAllConnected().size() >= 5)
+			return lineEnd;
+
+		return null;
+	}
+	
+	/**
+	 * Performs picking up of a single item, with special handling for lines.
+	 */
+	private void handlePickup(Item item, Point atPosition, boolean copy, boolean extrude)
+	{
+		// check permissions
+		if (copy && item.isLineEnd()) {
+			if (!item.hasPermission(UserAppliedPermission.full)) {
+				MessageBay.displayMessage("Insufficient permission to unreel");
+				return;
+			}
+		} else if (!item.hasPermission(UserAppliedPermission.full)) {
+			Item editTarget = item.getEditTarget();
+			if (editTarget != item && editTarget.hasPermission(UserAppliedPermission.full)) {
+				item = editTarget;
+			} else {
+				MessageBay.displayMessage("Insufficient permission to pick up item");
+				return;
+			}
+		}
+		
+		// Make a copy of the picked-up item
+		if (copy) {
+			List<Item> copies = ItemUtils.UnreelLine(item, false);
+			// Copies will NOT be null if the user right clicked on a point
+			if (copies == null) {
+				Collection<Item> originals = item.getConnected();
+				copies = ItemUtils.CopyItems(originals, extrude);
+				// if this is the title of the frame, link it to the frame
+				if (originals.size() == 1 && copies.size() == 1) {
+					Item firstCopy = copies.get(0);
+					Item original = originals.iterator().next();
+					if (original.getLink() == null && original.isFrameTitle()) {
+						// save the frame after copying
+						// i.getParent().setChanged(true);
+						firstCopy.setLink(original.getParentOrCurrentFrame().getName());
+					}
+				}
+	
+				FrameGraphics.changeHighlightMode(item, HighlightMode.None);
+	
+				if (!extrude) clearParent(copies);
+			}
+			
+			ItemUtils.Justify(copies);
+	
+			pickup(copies);
+			
+		// Just pick up the item without copying
+		} else {
+		
+			// BROOK: WIDGET RECTANGLES DONT ALLOW DISCONNECTION
+			if (item instanceof Line && !(item instanceof WidgetEdge)) {
+				// Check if within 20% of the end of the line
+				Line l = (Line) item;
+				Item toDisconnect = l.getEndPointToDisconnect(atPosition.getX(), atPosition.getY());
+	
+				if (toDisconnect == null) {
+					pickup(item);
+				} else {
+					if (toDisconnect.getHighlightMode() == Item.HighlightMode.Normal) {
+						DisplayController.setCursorPosition(toDisconnect.getPosition(), false);
+						pickup(toDisconnect);
+					} else {
+						List<Line> lines = toDisconnect.getLines();
+						// This is to remove constraints from single lines
+						// with constraints...
+						// ie. partially deleted rectangles
+						if (lines.size() == 1) {
+							toDisconnect.removeAllConstraints();
+	
+							DisplayController.setCursorPosition(toDisconnect.getPosition(), false);
+							// This is to ensure the selected mode will be set
+							// to Normal rather than disconnect when the line is
+							// anchored
+							toDisconnect.setHighlightMode(Item.HighlightMode.Normal);
+							pickup(toDisconnect);
+						} else {
+							// If we are then detatch the line and pick up its
+							// end point...
+							Frame currentFrame = DisplayController.getCurrentFrame();
+							Item newPoint = null;
+	
+							// If the point we are disconnecting is text...
+							// Then we want to leave the text behind
+							// And disconnect a point
+							if (toDisconnect instanceof Text) {
+								newPoint = new Dot(toDisconnect.getX(), toDisconnect.getY(), -1);
+								Item.DuplicateItem(toDisconnect, newPoint);
+							} else {
+								newPoint = toDisconnect.copy();
+							}
+	
+							currentFrame.addItem(newPoint);
+							// remove the current item from the connected
+							// list for this item
+							l.replaceLineEnd(toDisconnect, newPoint);
+							// remove unneeded constrains
+							newPoint.removeAllConstraints();
+	
+							// Set the new points mode to normal before picking
+							// it up so it will be restored correctly when
+							// anchored
+							newPoint.setHighlightMode(Item.HighlightMode.Normal);
+							toDisconnect.setHighlightMode(Item.HighlightMode.None);
+							DisplayController.setCursorPosition(toDisconnect.getPosition(), false);
+							pickup(newPoint);
+							ItemUtils.EnclosedCheck(toDisconnect.getParentOrCurrentFrame().getItems());
+						}
+					}
+				}
+			} else {
+				if (item.isLineEnd()) {
+					DisplayController.setCursorPosition(item.getPosition(), false);
+				}
+				pickup(item);
+			}
+		}
+	}
+	
+	/**
+	 * Performs picking up of a single item, with special handling for lines.
+	 */
+	private void handlePickup(Collection<Item> items, Collection<Item> enclosure, Point atPosition, boolean copy)
+	{
+		List<Item> toPickup = new ArrayList<Item>(items.size());
+		
+		if (copy) {
+			List<Item> copies = new ArrayList<Item>();
+			// Set the selection mode for the items that were clicked in
+			Collection<Item> enclosed = getFullyEnclosedItems(items);
+			if (enclosed.size() == 0) {
+				MessageBay.displayMessage("Insufficient permission to copy items");
+			} else {
+				copies = copy(enclosed);
+				clearParent(copies);
+				ItemUtils.Justify(copies);
+				pickup(copies);
+				for (Item i : items) {
+					i.setHighlightMode(HighlightMode.None);
+				}
+			}
+			toPickup = copies;
+		} else {
+			for (Item ip : items) {
+				if (ip.hasPermission(UserAppliedPermission.full)) toPickup.add(ip);
+			}
+		}
+		pickup(toPickup);
+	}
+
+	/**
+	 * Marks the items as not belonging to any specific frame. When picking up
+	 * items the parent will be automatically cleared for items on the current
+	 * frame but not for overlay items. This method ensures that overlay items
+	 * will also be cleared. This is useful when picking up copies of items from
+	 * an overlay (with the right mouse button) to ensure that the copy will be
+	 * anchored on the current frame rather than the overlay. When items are
+	 * picked up with the middle button clearParent should NOT be called.
+	 * 
+	 * @param items
+	 *            to have their parent cleared
+	 */
+	private static void clearParent(List<Item> items)
+	{
+		for (Item i : items) {
+			// The next line is only necessary for circles...
+			// Need to clean up/refactory some of this stuff
+			i.getParentOrCurrentFrame().removeItem(i);
+			i.setParent(null);
+		}
+	}
+	
+	private static boolean doMerging(Item clicked)
+	{
+		if (clicked == null) return false;
+
+		// // Brook: widgets do not merge
+		// if (clicked instanceof WidgetCorner)
+		// return false;
+		//
+		// // Brook: widgets do not merge
+		// if (clicked instanceof WidgetEdge)
+		// return false;
+
+		// System.out.println(FreeItems.getInstance().size());
+		if (isRubberBandingCorner()) {
+			if (clicked.isLineEnd()
+					|| clicked.getAllConnected().contains(
+							FreeItems.getItemAttachedToCursor())) {
+				return true;
+			}
+		}
+
+		if (FreeItems.getInstance().size() > 2)
+			return false;
+
+		Item attachedToCursor = FreeItems.getItemAttachedToCursor();
+
+		if (clicked instanceof Text
+				&& !(attachedToCursor instanceof Text || attachedToCursor
+						.isLineEnd())) {
+			return false;
+		}
+
+		if (clicked instanceof Picture) {
+			int mouseX = DisplayController.getMouseX();
+			int mouseY = DisplayController.getMouseY();
+			Picture clickedOnPicture = (Picture)clicked;
+			Frame current_frame = DisplayController.getCurrentFrame();
+			Colour bg_col = current_frame.getBackgroundColor();
+			if (clickedOnPicture.MouseOverBackgroundPixel(mouseX,mouseY,bg_col)) {
+				// Make 'clicked' null, effectively causing a back() operation
+				return false;
+			}
+		}
+		
+		return true;
+	}
+
+	private static boolean isRubberBandingCorner() {
+		return getShapeCorner(FreeItems.getInstance()) != null;
+	}
+	
+	public List<Item> deleteItemsAction(Item item) {
+		List<Item> items = new ArrayList<Item>();
+		// check permissions
+		if (!item.hasPermission(UserAppliedPermission.full)) {
+			//Items on the message box have parent == null
+			if (item.getParent() != null) {
+				if (!item.isFrameName()) {
+					Item editTarget = item.getEditTarget();
+					if (editTarget != item && editTarget.hasPermission(UserAppliedPermission.full)) {
+						item = editTarget;
+					} else {
+						MessageBay.displayMessage("Insufficient permission");
+						return items;
+					}
+				}
+				
+			} else /*Its in the message area*/ {
+				MessageBay.displayMessage("Insufficient permission");
+				return items;
+			}
+		}
+		Item merger = FreeItems.getItemAttachedToCursor();
+		assert (merger != null);
+		Collection<Item> left = null;
+		// when anchoring a line end onto a text line end, holding shift
+		// prevents the line ends from being merged
+		if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.SHIFT)) {
+			left = FreeItems.getInstance();
+		} else {
+			left = merge(FreeItems.getInstance(), item);
+		}
+		Collection<Item> toDelete = new LinkedList<Item>();
+		toDelete.addAll(FreeItems.getInstance());
+		toDelete.removeAll(left);
+		anchor(left);
+		FreeItems.getInstance().clear();
+		DisplayController.getCurrentFrame().removeAllItems(toDelete);
+		updateCursor();
+		// Make sure the dot goes away when anchoring a line end behind
+		// a text line end
+		if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.SHIFT)) {
+			refreshHighlights();
+		}
+		DisplayController.requestRefresh(true);
+		return items;
+		// otherwise, anchor the items
+	}
+
+	private static final int RECTANGLE_TO_POINT_THRESHOLD = 20;
+	
+	public static Collection<Item> merge(List<Item> merger, Item mergee) {
+		assert (mergee != null);
+		if (mergee.isFrameName()) {
+			return mergee.getParent().merge(merger);
+		}
+
+		// if(mergee instanceof XRayable)
+		// return merger;
+
+		// check for rectangle merging
+		if (merger.size() == 3 && mergee.getLines().size() == 2) {
+			Item corner = getShapeCorner(merger);
+			// if this is a corner of a shape
+			if (corner != null) {
+				Collection<Item> allConnected = corner.getAllConnected();
+				// Check if we are collapsing a rectangle
+				if (allConnected.size() == 8 && allConnected.contains(mergee)) {
+					DisplayController.setCursorPosition(mergee.getPosition());
+					DisplayController.getCurrentFrame().removeAllItems(allConnected);
+
+					// find the point opposite corner...
+					Item opposite = null;
+					List<Line> lines = corner.getLines();
+					for (Line l : lines) {
+						allConnected.remove(l.getOppositeEnd(corner));
+					}
+					allConnected.remove(corner);
+					for (Item i : allConnected) {
+						if (i.isLineEnd()) {
+							opposite = i;
+							break;
+						}
+					}
+					assert (opposite != null);
+
+					// check if the rectangle is small enough that it should be
+					// collapsed to a single point
+					int x1 = Math.abs(opposite.getX() - mergee.getX());
+					int x2 = Math.abs(opposite.getY() - mergee.getY());
+					int distance = (int) Math.sqrt(Math.pow(x1, 2)
+							+ Math.pow(x2, 2));
+
+					if (distance < RECTANGLE_TO_POINT_THRESHOLD) {
+						mergee.removeAllConstraints();
+						mergee.removeAllLines();
+						mergee.setThickness(4 * mergee.getThickness());
+						return mergee.getAllConnected();
+					} else {
+						removeAllLinesExcept(mergee, opposite);
+						removeAllLinesExcept(opposite, mergee);
+
+						return mergee.getAllConnected();
+					}
+				}
+			}
+		}
+
+		List<Item> remain = new ArrayList<Item>();
+		Item res = null;
+
+		for (Item i : merger) {			
+			if (!i.isVisible())
+				continue;
+			// check for link merging
+			if (i instanceof Text
+					&& FrameIO.isValidFrameName((((Text) i).getFirstLine()))
+					&& FrameIO.canAccessFrame((((Text) i).getFirstLine()))) {
+				// check that we can actually access the frame this link
+				// corresponds to
+				mergee.setLink(((Text) i).getFirstLine());
+			} else {
+				// check for attribute merging
+				if (i instanceof Text && !i.isLineEnd()) {
+					Text txt = (Text) i;
+
+					// if this is not an attribute merge
+					if (!AttributeUtils.setAttribute(mergee, txt)) {
+						// set mouse position for text merges
+						if (mergee instanceof Text) {
+							((Text) mergee).insertText(txt.getText(), DisplayController.getMouseX(), DisplayController.getMouseY());
+							//Delete the item which had its text merged
+							txt.delete();
+							return remain;
+						} else if (mergee instanceof WidgetCorner) {
+							if (merger.size() == 1 && txt.getLink() != null) {
+								// If the text item is linked then use that
+								((WidgetCorner) mergee).setLink(txt
+										.getAbsoluteLink(), txt);
+							} else {
+								remain.addAll(merger);
+							}
+							return remain;
+						} else if (mergee instanceof WidgetEdge) {
+							if (merger.size() == 1 && txt.getLink() != null) {
+								// If the text item is linked then use that
+								((WidgetEdge) mergee).setLink(txt
+										.getAbsoluteLink(), txt);
+							} else {
+								remain.addAll(merger);
+							}
+							return remain;
+						} else if (mergee instanceof Dot) {
+							DisplayController.setCursorPosition(mergee.getPosition());
+							txt.setPosition(mergee.getPosition());
+							txt.setThickness(mergee.getThickness());
+							Frame parent = mergee.getParent();
+							parent.removeItem(mergee);
+							anchor(txt);
+							// change the end points of the lines to the text
+							// item
+							while (mergee.getLines().size() > 0) {
+								Line l = mergee.getLines().get(0);
+								l.replaceLineEnd(mergee, txt);
+							}
+							break;
+						}
+
+						// TODO tidy this up...
+						// Dot override doesnt use the x and y coords
+						// Text does... but could be removed
+						res = mergee.merge(i, DisplayController.getMouseX(), DisplayController.getMouseY());
+						if (res != null) {
+							remain.add(res);
+						}
+					}
+				} else {
+					if (mergee.isLineEnd()) {
+						DisplayController.setCursorPosition(mergee.getPosition());
+					}
+					// Moving the cursor ensures shapes are anchored correctly
+					res = mergee.merge(i, DisplayController.getMouseX(), DisplayController.getMouseY());
+					if (res != null)
+						remain.addAll(res.getConnected());
+
+				}
+			}
+		}
+		updateCursor();
+
+		mergee.getParent().setChanged(true);
+
+		ItemUtils.EnclosedCheck(mergee.getParent().getItems());
+		// Mike: Why does parse frame have to be called?!?
+		FrameUtils.Parse(mergee.getParent());
+
+		return remain;
+	}
+	
+	private static void removeAllLinesExcept(Item from, Item but) {
+		List<Line> lines = new LinkedList<Line>();
+		lines.addAll(from.getLines());
+		for (Line line : lines)
+			if (line.getOppositeEnd(from) != but)
+				from.removeLine(line);
+
+		List<Constraint> consts = new LinkedList<Constraint>();
+		consts.addAll(from.getConstraints());
+		for (Constraint c : consts)
+			if (c.getOppositeEnd(from) != but)
+				from.removeConstraint(c);
+	}
+	
+	public void anchorFreeItemsAction(Collection<Item> items) {
+		if (items != null && FreeItems.getInstance().size() == 1) {
+			Item item = FreeItems.getItemAttachedToCursor();
+			if (item instanceof Text) {
+				Text text = (Text) item;
+				if (AttributeUtils.setAttribute(text, text, 2)) {
+					items.removeAll(FrameUtils.getEnclosingLineEnds().iterator().next().getAllConnected());
+					for (Item i : items) {
+						AttributeUtils.setAttribute(i, text);
+					}
+					FreeItems.getInstance().clear();
+				}
+			}
+		}
+
+		// if a line is being rubber-banded, check for auto
+		// straightening
+		anchor(FreeItems.getInstance());
+		FreeItems.getInstance().clear();
+		updateCursor();
+		_offX = _offY = 0;
+	}
+	
+	public void copyPlace(Item item)
+	{
+		List<Item> copies = null;
+		if (FreeItems.getInstance().size() == 1 && FreeItems.getItemAttachedToCursor().isAutoStamp()) {
+			// Dont stamp if the user is painting... because we dont want to
+			// save any of the items created!
+			return;
+		// if the user is clicking on something, merge the items unless
+		// it is a point onto something other than a lineEnd or a dot
+		} else if (item != null && (!(FreeItems.getItemAttachedToCursor() instanceof Dot) || item instanceof Dot || item.isLineEnd())) {
+		// TODO Change the items merge methods so the logic is simplified		
+			// check permissions
+			if (!item.hasPermission(UserAppliedPermission.full) && item.getParent().getNameItem() != item) {
+				MessageBay.displayMessage("Insufficient permission to merge items");
+				return;
+			}
+			if (item instanceof Text || item instanceof Dot || item instanceof XRayable) {
+				if (isRubberBandingCorner()) {
+					copies = mergeGroupAction(item);
+					// line onto something
+				} else if (FreeItems.getInstance().size() == 2 /* && clicked instanceof XRayable */) {
+					copies = mergeTwoItemsAction(item);
+				} else if (FreeItems.getInstance().size() == 1) {
+					copies = mergeSingleItemAction(item);
+				} else {
+					copies = stampAction();
+				}
+			} else {
+				copies = ItemUtils.UnreelLine(FreeItems.getInstance(), false);
+				if (copies == null) copies = copy(FreeItems.getInstance());
+				for (Item i : copies) {
+					i.setOffset(0, 0);
+				}
+				anchor(FreeItems.getInstance());
+				FreeItems.getInstance().clear();
+				pickup(copies);
+			}
+		// otherwise, anchor the items
+		} else {
+			// check if this is anchoring a rectangle
+			if (isRubberBandingCorner()) {
+				copies = rubberBandingCornerAction();
+			} else {
+				if (FreeItems.rubberBanding()) {
+					copies = rubberBandingCopyAction(item);
+				} else {
+					stampItemsOnCursor(true);
+					copies = FreeItems.getInstance();
+				}
+			}
+		}
+	}
+
+	/**
+	 * Returns a list of copies of the list passed in
+	 * 
+	 * @param toCopy
+	 *            The list of items to copy
+	 * @return A List of copied Items
+	 */
+	private static List<Item> copy(Collection<Item> toCopy) {
+		return ItemUtils.CopyItems(toCopy);
+	}
+	
+	public List<Item> mergeGroupAction(Item item) {
+		List<Item> copies = new ArrayList<Item>();
+		// Move the cursor so that the copy is exactly the
+		// same as the shape that was anchored
+		DisplayController.setCursorPosition(item.getPosition());
+		Item d = getFirstFreeLineEnd();
+		// get a copy of all enclosed items before merging
+		// lineEnds
+		Collection<Item> items = FrameUtils.getItemsEnclosedBy(
+				DisplayController.getCurrentFrame(), d
+						.getEnclosedShape());
+		// If its not an enclosed shape then pick up the
+		// connected shape
+		if (items == null || items.size() == 0) {
+			items = d.getAllConnected();
+		} else {
+			// For some reason the item that was clicked ends up
+			// in the enclosure and needs to be removed
+			items.removeAll(item.getConnected());
+			// the item that was the origin of the enclosed
+			// shape used to create the enclosure does not get
+			// returned from getItemsEnclosedBy to the enclosure
+			// so it must be added
+			items.addAll(d.getConnected());
+		}
+
+		Collection<Item> toCopy = new LinkedHashSet<Item>();
+
+		for (Item ip : items) {
+			if (ip.hasPermission(UserAppliedPermission.copy))
+				toCopy.add(ip);
+		}
+		copies = copy(toCopy);
+		// Now do the merging
+		Collection<Item> remain = merge(FreeItems.getInstance(), item);
+		// anchor the points
+		anchor(remain);
+		FreeItems.getInstance().clear();
+		pickup(copies);
+		return copies;
+	}
+
+	private static Item getFirstFreeLineEnd()
+	{
+		for (Item i : FreeItems.getInstance())
+			if (i.isLineEnd())
+				return i;
+		return null;
+	}	
+	
+	public List<Item> mergeTwoItemsAction(Item item) {
+		List<Item> copies = new ArrayList<Item>();
+		copies = ItemUtils.UnreelLine(FreeItems.getInstance(), false);
+		Collection<Item> leftOver = merge(FreeItems.getInstance(), item);
+		anchor(leftOver);
+		if (copies == null)
+			copies = copy(FreeItems.getInstance());
+		FreeItems.getInstance().clear();
+		for (Item i : copies)
+			i.setOffset(0, 0);
+		// need to move to prevent cursor dislocation
+		move(copies, EcosystemManager.getInputManager().getCursorPosition());
+		pickup(copies);
+		// point onto point
+		return copies;
+	} 	
+	
+	public List<Item> mergeSingleItemAction(Item item) {
+		List<Item> copies = new ArrayList<Item>();
+		copies = copy(FreeItems.getInstance());
+		Collection<Item> remain = merge(copies, item);
+
+		// ignore items that could not be merged.
+		anchor(remain);
+		return copies;
+	}  
+	
+	public List<Item> stampAction() {
+		List<Item> copies = new ArrayList<Item>();
+		stampItemsOnCursor(true);
+		copies = FreeItems.getInstance();
+		return copies;
+	}	
+
+	/**
+	 * 
+	 */
+	private static void stampItemsOnCursor(boolean save) {
+		List<Item> copies = copy(FreeItems.getInstance());
+		// MIKE: what does the below 2 lines do?
+		for (Item i : copies) {
+			i.setOffset(0, 0);
+			i.setSave(save);
+		}
+		// The below code has a little problem withflicker when stamp and dragging
+		move(FreeItems.getInstance(), EcosystemManager.getInputManager().getCursorPosition());
+		for (Item i : copies) {
+			i.setHighlightMode(HighlightMode.None);
+		}
+		anchor(copies);
+	}
+	
+	public List<Item> rubberBandingCornerAction() {
+		List<Item> copies = new ArrayList<Item>();
+		Item d = getFirstFreeLineEnd();
+		// anchor the points
+		anchor(FreeItems.getInstance());
+		FreeItems.getInstance().clear();
+		updateCursor();
+		// pick up a copy of all enclosed items
+		Collection<Item> enclosedItems = FrameUtils.getItemsEnclosedBy(DisplayController.getCurrentFrame(), d.getEnclosedShape());
+		if (enclosedItems != null) {
+			enclosedItems.removeAll(d.getAllConnected());
+			Collection<Item> toCopy = getFullyEnclosedItems(enclosedItems);
+
+			if (toCopy.size() > 0) {
+				// Find the closest item to the mouse cursor
+				double currentX = DisplayController.getMouseX();
+				double currentY = DisplayController.getMouseY();
+				Item closest = null;
+				double shortestDistance = Double.MAX_VALUE;
+				for (Item next : toCopy) {
+					if (next instanceof Line)
+						continue;
+					double distance = Point.distanceBetween((int) currentX, (int) currentY, next.getX(), next.getY());
+					if (distance < shortestDistance) {
+						shortestDistance = distance;
+						closest = next;
+					}
+				}
+				// Move the cursor to closest item
+				DisplayController.setCursorPosition(closest.getPosition());
+				// Pickup copy of the stuff inside the rectangle
+				copies = copy(toCopy);
+				pickup(copies);
+				// Remove the rectangle
+				d.getParentOrCurrentFrame().removeAllItems(
+						d.getAllConnected());
+			} else {
+				// Pick up a copy of the rectangle
+				copies = copy(d.getAllConnected());
+				pickup(copies);
+			}
+		}
+		return copies;
+	}		
+
+	/**
+	 * @param enclosedItems
+	 * @return
+	 */
+	private static Collection<Item> getFullyEnclosedItems(Collection<Item> enclosure)
+	{
+		// copy the enclosedItems because the list will be modified
+		Collection<Item> enclosedItems = new LinkedHashSet<Item>(enclosure);
+		Collection<Item> toCopy = new LinkedHashSet<Item>(enclosedItems.size());
+
+		while (enclosedItems.size() > 0) {
+			Item i = enclosedItems.iterator().next();
+			if (i.hasPermission(UserAppliedPermission.copy)) {
+				Collection<Item> items = i.getAllConnected();
+				// Only copy if the entire shape is enclosed
+				if (enclosedItems.containsAll(items)) {
+					toCopy.addAll(items);
+				}
+				enclosedItems.removeAll(items);
+			} else {
+				enclosedItems.remove(i);
+			}
+		}
+		return toCopy;
+	}
+	
+	public List<Item> rubberBandingCopyAction(Item item) {
+		List<Item> copies = new ArrayList<Item>();
+		if (item != null) {
+			Collection<Item> leftOver = merge(FreeItems.getInstance(), item);
+			anchor(leftOver);
+		}
+		// This is executed when the user is putting down a line
+		// endpoint and unreeling. ie. Normal unreeling
+		copies = ItemUtils.UnreelLine(FreeItems.getInstance(), false);
+
+		if (copies == null) copies = copy(FreeItems.getInstance());
+		
+		anchor(FreeItems.getInstance());
+		for (Item i : copies) i.setOffset(0, 0);
+		// need to move to prevent cursor dislocation
+		move(copies, EcosystemManager.getInputManager().getCursorPosition());
+		pickup(copies);
+		return copies;
+	}	
+	
+	public List<Item> newLineAction(Point position, Item item) {
+		List<Item> copies = new ArrayList<Item>();
+		// If we have permission to copy this item then pick it up
+		if (item != null && item.isLineEnd() && item.hasPermission(UserAppliedPermission.full)) {
+			item.removeAllConstraints();
+			pickup(item);
+			return copies;
+		}
+
+		if (item instanceof WidgetEdge) {
+			// Don't allow the user to break widget edges.
+			// Note: had to return here because random dots would
+			// appear otherwise... cannot understand code below
+			// with create line.
+			return copies;
+		}
+
+		// if its on a line then split the line and put a point on it and
+		// pick that point up. Only if it is not a widget line
+		if (item instanceof Line && item.hasPermission(UserAppliedPermission.full)) {
+			Frame current = DisplayController.getCurrentFrame();
+			// create the two endpoints
+			Line oldLine = (Line) item;
+			Item newPoint = oldLine.getStartItem().copy();
+			newPoint.setPosition(position);
+
+			Item end = oldLine.getEndItem();
+			// create the Line
+			Line newLine = new Line(newPoint, end, current.getNextItemID());
+			oldLine.replaceLineEnd(end, newPoint);
+			newPoint.removeAllConstraints();
+			pickup(newPoint);
+			// Update the stats
+			Collection<Item> created = new LinkedList<Item>();
+			created.add(newPoint);
+			created.add(newLine);
+			SessionStats.CreatedItems(newLine.getAllConnected());
+			return copies;
+		}
+		Line newLine = createLine();
+		SessionStats.CreatedItems(newLine.getAllConnected());
+		return copies;
+	}
+
+	private static Line createLine() {
+		Frame current = DisplayController.getCurrentFrame();
+		// create the two endpoints
+		Item end = DisplayController.getCurrentFrame().createDot();
+		Item start = DisplayController.getCurrentFrame().createDot();
+
+		// create the Line
+		Line line = new Line(start, end, current.getNextItemID());
+		line.autoArrowheadLength();
+
+		// anchor the start
+		anchor(start);
+
+		// attach the line to the cursor
+		pickup(end);
+		_lastHighlightedItem = null;
+
+		// TODO figure out how to get the end to highlight
+		// end.setSelectedMode(SelectedMode.Normal);
+		// end.setSelectedMode(SelectedMode.None);
+
+		return line;
+	}
+	
+	public List<Item> createRectangleAction() {
+		final List<Item> copies = new ArrayList<Item>();
+		Item[] d = new Item[4];
+		// create dots
+		Frame current = DisplayController.getCurrentFrame();
+		for (int i = 0; i < d.length; i++) {
+			d[i] = current.createDot();
+			copies.add(d[i]);
+		}
+
+		current.nextDot();
+
+		// create lines
+		copies.add(new Line(d[0], d[1], current.getNextItemID()));
+		copies.add(new Line(d[1], d[2], current.getNextItemID()));
+		copies.add(new Line(d[2], d[3], current.getNextItemID()));
+		copies.add(new Line(d[3], d[0], current.getNextItemID()));
+
+		new Constraint(d[0], d[1], current.getNextItemID(), Constraint.HORIZONTAL);
+		new Constraint(d[2], d[3], current.getNextItemID(), Constraint.HORIZONTAL);
+		new Constraint(d[1], d[2], current.getNextItemID(), Constraint.VERTICAL);
+		new Constraint(d[3], d[0], current.getNextItemID(), Constraint.VERTICAL);
+
+		anchor(new ArrayList<Item>(copies));
+		pickup(d[3]);
+		d[3].setHighlightMode(HighlightMode.Normal);
+
+		SessionStats.CreatedItems(copies);
+		copies.clear();
+		return copies;
+	} 
+
+	/**
+	 * This method handles all left-click actions
+	 */
+	private void click(Item clicked, Collection<Item> clickedIn, Point position)
+	{
+		
+		//Gets the current frame
+		Frame f = DisplayController.getCurrentFrame();
+		
+		//Checks if the current frame is an overlay
+		if (f.getOverlays() != null && FrameUtils.getCurrentItem() != null) {
+			Item i = FrameUtils.getCurrentItem();
+			
+			//Checks if the item clicked in the overlay is a Rubbish Bin. If it is, delete the item attached to the cursor and return.
+			if(i instanceof WidgetCorner){
+				
+				try{
+					WidgetCorner wc = (WidgetCorner)i;					
+					ButtonWidget bw = (ButtonWidget) wc.getWidgetSource();
+					
+					//Should call a button widgets 'itemheldwhileclicked' method, and process depending on the widget type - else will return false.
+					if(bw.itemHeldWhileClicked((Widget)bw) == true){									
+						
+						return;
+					}				
+				}
+				catch (Exception e){
+					
+					e.printStackTrace();
+				}
+			}
+		}
+		
+
+		// if the user is pointing at something then either follow the link or
+		// do TDFC
+		if (clicked == null) {
+			// Check if the user is nearby another item...
+			int mouseX = position.x;
+			int mouseY = position.y;
+			// System.out.println(mouseX + "," + mouseY);
+			for (Item i : DisplayController.getCurrentFrame().getItems()) {
+				//System.out.println(i.getName().toString());
+				if (i instanceof Text) {
+					if (i.isNear(mouseX, mouseY)) {
+						clicked = i;
+						break;
+					}
+				}
+			}
+		}
+
+		if (clicked instanceof Text) {
+			Text text = (Text) clicked;
+			/* Don't follow link when just highlighting text with the left button */
+			if (text.getText().length() == 0)
+				clicked = null;
+			else if (text.getSelectionSize() > 0) {
+				return;
+			}
+		}
+		
+		// If the user clicked into a widgets free space...
+		if (clicked == null && clickedIn != null && clickedIn.size() >= 4) {
+
+			// Check to see if the user clicked into a widgets empty space
+			Widget iw = null;
+
+			for (Item i : clickedIn) {
+
+				if (i instanceof WidgetCorner) {
+					iw = ((WidgetCorner) i).getWidgetSource();
+					break;
+				} else if (i instanceof WidgetEdge) {
+					iw = ((WidgetEdge) i).getWidgetSource();
+					break;
+				}
+			}
+
+			if (iw != null) {
+
+				// Handle dropping items on widgets
+				if(iw.ItemsLeftClickDropped()) {
+					return;
+				}
+				
+				// Note: mustn't directly use source for handling the link
+				// because all link operations will by-pass the widgets special
+				// handling with links...
+				Item widgetLink = iw.getItems().get(0);
+				assert (widgetLink != null);
+				clicked = widgetLink;
+			} else {
+				for (Item i : clickedIn) {
+					/*
+					 * Find the first linked item or the first unlinked Dot This
+					 * code assumes that items are are ordered from top to
+					 * bottom. TODO make sure the list will always be ordered
+					 * correctly!!
+					 */
+					if (i.hasLink() || i instanceof Dot) {
+						clicked = i;
+						break;
+					}
+				}
+			}
+
+		}
+
+		
+		if (clicked instanceof Picture) {
+			int mouseX = position.x;
+			int mouseY = position.y;
+			Picture clickedOnPicture = (Picture)clicked;
+			Frame current_frame = DisplayController.getCurrentFrame();
+			Colour bg_col = current_frame.getBackgroundColor();
+			if (clickedOnPicture.MouseOverBackgroundPixel(mouseX,mouseY,bg_col)) {
+				// Make 'clicked' null, effectively causing a back() operation
+				clicked = null;
+			}
+		}
+		
+		// This makes it so clicking repeatedly on the frameName doesn't add the
+		// frames to the backup stack. Only the first frame is added to the
+		// backup stack.
+		if (!(clicked != null && clicked.isFrameName())) {
+			Navigation.ResetLastAddToBack();
+		}
+		
+		if (clicked != null) {
+			// check item permissions
+			boolean hasLinkOrAction = clicked.hasLink() || clicked.hasAction();
+
+			if (	(hasLinkOrAction && !clicked.hasPermission(UserAppliedPermission.followLinks)) ||
+					(!hasLinkOrAction && !clicked.hasPermission(UserAppliedPermission.createFrames)))
+			{
+				Item editTarget = clicked.getEditTarget();
+				if (editTarget != clicked) {
+					if (editTarget.hasPermission(UserAppliedPermission.followLinks)) {
+						clicked = editTarget;
+					} else {
+						MessageBay.displayMessage("Insufficient permission to perform action on item");
+						return;
+					}
+				}
+			}
+
+			Item clickedOn = clicked;
+
+			// actions take priority
+			if (clickedOn.hasAction()) {
+				executeActionAction(clicked);
+			} else if (clickedOn.getLink() != null) {
+				followLinkAction(clicked);
+			// no link is found, perform TDFC
+			} else {
+				/*
+				 * if the user is clicking on the frame name then move to the
+				 * next or previous frame regardless of whether or not the frame
+				 * is protected
+				 */
+				if (clickedOn.isFrameName()) {
+					respondToFrameNameClickAction();
+				}
+				
+				TDFCAction(clicked);
+			}
+
+		} else {
+			backAction();
+		}
+	}
+	
+	public void executeActionAction(Item item) {
+		item.performActions();
+		item.setHighlightMode(HighlightMode.None);
+		refreshHighlights();
+	}
+	
+	public void followLinkAction(Item item) {
+		/*
+		 * Dont save the frame if we are moving to an old version of
+		 * this frame because everytime we save with the old tag... the
+		 * frame is backed up
+		 */
+		if (!item.isOldTag()) FrameIO.SaveFrame(DisplayController.getCurrentFrame());
+
+		Navigation.setLastNavigationItem(item);
+		load(item.getAbsoluteLink(), item.getLinkHistory());
+		// DisplayIO.UpdateTitle();
+	}
+	
+	private static void load(String toLoad, boolean addToHistory) {
+		if (FrameIO.isValidFrameName(toLoad)) {
+			DisplayController.clearBackedUpFrames();
+			FrameUtils.DisplayFrame(toLoad, addToHistory, true);
+		} else {
+			MessageBay.errorMessage(toLoad + " is not a valid frame name.");
+		}
+	}
+	
+	public void respondToFrameNameClickAction()
+	{
+		if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.CTRL)) {
+			Navigation.PreviousFrame(false);
+		} else {
+			Navigation.NextFrame(false);
+		}
+	} 	
+	
+	public List<Item> TDFCAction(Item item) {
+		// check for TDFC permission
+		if (!item.hasPermission(UserAppliedPermission.createFrames)) {
+			MessageBay.displayMessage("Insufficient permission to TDFC (Top Down Frame Creation) from that item");
+			return null;
+		}
+
+		if (item.isOldTag())
+			return null;
+
+		try {
+			tdfc(item);
+		} catch (RuntimeException e) {
+			e.printStackTrace();
+			MessageBay.errorMessage("Top Down Frame Creation (TDFC) error: " + e.getMessage());
+		}
+		return null;
+	}
+	
+	public void backAction()
+	{
+		//if user is not pointing at something, this is a back
+		if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.CTRL) || StandardInputEventListeners.kbmStateListener.isKeyDown(Key.SHIFT))
+			forward();
+		else
+			back();
+	}
+
+	private static void forward() {
+		DisplayController.Forward();
+
+		// repaint things if necessary
+		if (FreeItems.hasItemsAttachedToCursor())
+			move(FreeItems.getInstance(), EcosystemManager.getInputManager().getCursorPosition());
+
+		if (FreeItems.hasCursor())
+			move(FreeItems.getCursor(), EcosystemManager.getInputManager().getCursorPosition(), true);
+	}
+
+	private static void back() {
+		DisplayController.Back();
+
+		// repaint things if necessary
+		if (FreeItems.hasItemsAttachedToCursor())
+			move(FreeItems.getInstance(), EcosystemManager.getInputManager().getCursorPosition());
+
+		if (FreeItems.hasCursor())
+			move(FreeItems.getCursor(), EcosystemManager.getInputManager().getCursorPosition(), true);
+	}
+
+	/**
+	 * Creates a new Text item and fills it with particular attributes extracted
+	 * from the given Item. Note: Users always have permission to extract
+	 * attributes, so it is not checked.
+	 * 
+	 * @param toExtract
+	 *            Item containing the Item to extract the attributes from.
+	 */
+	private static void extractAttributes(Item toExtract)
+	{
+		if (toExtract == null || toExtract == null) return;
+
+		if (FreeItems.hasItemsAttachedToCursor()) return;
+
+		Item attribs;
+		Item item = toExtract;
+		// Extract the frames attributes when the user clicks on the frame name
+		FrameGraphics.changeHighlightMode(item, HighlightMode.None);
+		if (item.isFrameName())
+			attribs = AttributeUtils.extractAttributes(item.getParent());
+		else {
+			attribs = AttributeUtils.extractAttributes(item);
+		}
+
+		if (attribs == null)
+			MessageBay.displayMessage("All attributes of that item are default values.");
+		else {
+			// Give the attribute text item the color of the item for which
+			// attributes are being extracted.
+			// attribs.setColor(item.getColor());
+			pickup(attribs);
+		}
+	}
+	
+	private void pickupRange(Text text, Point position, boolean copy, boolean inheritAttributes)
+	{
+		Text ranged;
+		if (inheritAttributes) {
+			// If shift is down, copy everything (size, color, etc.) except actions, links and data
+			ranged = text.copy();
+			ranged.setActions(null);
+			ranged.setData((List<String>) null);
+			ranged.setLink(null);
+		} else {
+			// If shift isn't down, don't copy any attributes, but base the new text item on the appropriate template
+			ranged = DisplayController.getCurrentFrame().getItemTemplate(text.copySelectedText().charAt(0));
+		}
+
+		// if the user is cutting text from the item
+		if (!copy) {
+			// Check if the user is trying to range an item for which they
+			// do not have permission to do so... or it is the frame name
+			if (!text.hasPermission(UserAppliedPermission.full) || text.isFrameName()) {
+				MessageBay.displayMessage("Insufficient permission to cut text");
+				text.clearSelection();
+				DisplayController.requestRefresh(true);
+				return;
+			}
+			// if the entire text is selected and its not a line end then pickup the item
+			boolean entireText = text.getSelectionSize() == text.getLength();
+			if (entireText && !text.isLineEnd()) {
+				text.clearSelection();
+				ranged.delete();
+				handlePickup(text, position, false, false);
+				return;
+			} else {
+				ranged.setText(text.cutSelectedText());
+				ranged.setWidth(text.getWidth());
+				// If its the whole text then replace last ranged with a dot
+				if (entireText) {
+					Item dot = Item.replaceText(text);
+					dot.setHighlightMode(HighlightMode.None);
+				}
+			}
+			// if the user is copying text from the item
+		} else {
+			// Check if the user is trying to range an item for which they
+			// do not have permission to do so... or it is the frame name
+			if (!text.hasPermission(UserAppliedPermission.copy)) {
+				MessageBay.displayMessage("Insufficient permission to copy text");
+				text.clearSelection();
+				DisplayController.requestRefresh(true);
+				return;
+			}
+			ranged.setText(text.copySelectedText());
+		}
+
+		ranged.setParent(null);
+		ranged.setPosition(position);
+		pickup(ranged);
+		text.clearSelection();
+		text.setHighlightMode(HighlightMode.None);
+		refreshHighlights();
+		DisplayController.requestRefresh(false);
+		return;
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/ChangeColourGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/ChangeColourGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/ChangeColourGestureData.java	(revision 1097)
@@ -0,0 +1,37 @@
+package org.expeditee.gio.gesture.data;
+
+public class ChangeColourGestureData extends ItemSpecificGestureData {
+
+	private boolean _setTransparent;
+	private boolean _setSecondaryColour;
+	
+	public ChangeColourGestureData(boolean setTransparent, boolean setSecondaryColour)
+	{
+		super();
+		_setTransparent = setTransparent;
+		_setSecondaryColour = setSecondaryColour;
+	}
+	
+	public ChangeColourGestureData(ChangeColourGestureData other)
+	{
+		super(other);
+		_setTransparent = other._setTransparent;
+		_setSecondaryColour = other._setSecondaryColour;
+	}
+	
+	public boolean shouldSetTransparent()
+	{
+		return _setTransparent;
+	}
+	
+	public boolean shouldSetSecondaryColour()
+	{
+		return _setSecondaryColour;
+	}
+	
+	@Override
+	public GestureData clone()
+	{
+		return new ChangeColourGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/ClickGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/ClickGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/ClickGestureData.java	(revision 1097)
@@ -0,0 +1,28 @@
+package org.expeditee.gio.gesture.data;
+
+public class ClickGestureData extends ItemSpecificGestureData {
+
+	private boolean _longClick;
+	
+	public ClickGestureData(boolean longClick)
+	{
+		super();
+		_longClick = longClick;
+	}
+	
+	public ClickGestureData(ClickGestureData other)
+	{
+		super(other);
+		if (other != null) _longClick = other._longClick;
+	}
+	
+	public boolean getLongClick()
+	{
+		return _longClick;
+	}
+	
+	public ClickGestureData clone()
+	{
+		return new ClickGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/CreateItemGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/CreateItemGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/CreateItemGestureData.java	(revision 1097)
@@ -0,0 +1,33 @@
+package org.expeditee.gio.gesture.data;
+
+public class CreateItemGestureData extends ItemSpecificGestureData {
+
+	public enum ItemType {
+		LINE,
+		BOX
+	}
+	
+	private ItemType _itemType;
+	
+	public CreateItemGestureData(ItemType itemType)
+	{
+		super();
+		_itemType = itemType;
+	}
+	
+	public CreateItemGestureData(CreateItemGestureData other)
+	{
+		super(other);
+		if (other != null) _itemType = other._itemType;
+	}
+	
+	public ItemType getItemType()
+	{
+		return _itemType;
+	}
+	
+	public CreateItemGestureData clone()
+	{
+		return new CreateItemGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/DeleteGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/DeleteGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/DeleteGestureData.java	(revision 1097)
@@ -0,0 +1,29 @@
+package org.expeditee.gio.gesture.data;
+
+public class DeleteGestureData extends ItemSpecificGestureData {
+	
+	private boolean _alternateMode;
+	
+	public DeleteGestureData(boolean alternateMode)
+	{
+		super();
+		_alternateMode = alternateMode;
+	}
+	
+	public DeleteGestureData(DeleteGestureData other)
+	{
+		super(other);
+		_alternateMode = other._alternateMode;
+	}
+	
+	public boolean getAlternateMode()
+	{
+		return _alternateMode;
+	}
+	
+	public DeleteGestureData clone()
+	{
+		return new DeleteGestureData(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/FormatGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/FormatGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/FormatGestureData.java	(revision 1097)
@@ -0,0 +1,47 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.gui.DisplayController;
+import org.expeditee.gui.Frame;
+
+public class FormatGestureData extends GestureData {
+	
+	private Frame _frame;
+	private boolean _horizontal;
+	
+	public FormatGestureData(boolean horizontal)
+	{
+		super();
+		_frame = DisplayController.getCurrentFrame();
+		_horizontal = horizontal;
+	}
+	
+	public FormatGestureData(Frame frame, boolean horizontal)
+	{
+		super();
+		_frame = frame;
+		_horizontal = horizontal;
+	}
+	
+	public FormatGestureData(FormatGestureData other)
+	{
+		super(other);
+		_frame = other._frame;
+		_horizontal = other._horizontal;
+	}
+	
+	public Frame getFrame()
+	{
+		return _frame;
+	}
+	
+	public boolean getHorizontal()
+	{
+		return _horizontal;
+	}
+	
+	public FormatGestureData clone()
+	{
+		return new FormatGestureData(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/GestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/GestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/GestureData.java	(revision 1097)
@@ -0,0 +1,39 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.core.Point;
+import org.expeditee.gio.EcosystemManager;
+
+public class GestureData {
+
+	/** All gestures have a position. */
+	protected Point _position = null;
+	
+	public GestureData()
+	{
+		// Use the current mouse position
+		_position = EcosystemManager.getInputManager().getCursorPosition();
+	}
+	
+	public GestureData(Point position)
+	{
+		if (position != null) _position = position.clone();
+	}
+	
+	public GestureData(GestureData other)
+	{
+		if (other != null && other._position != null) _position = other._position.clone();
+	}
+	
+	public Point getPosition()
+	{
+		if (_position == null) return null;
+		
+		return _position.clone();
+	}
+	
+	public GestureData clone()
+	{
+		return new GestureData(this);
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/InsertStringGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/InsertStringGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/InsertStringGestureData.java	(revision 1097)
@@ -0,0 +1,45 @@
+package org.expeditee.gio.gesture.data;
+
+public class InsertStringGestureData extends ItemSpecificGestureData {
+	
+	private String _string;
+	private boolean _shiftDown; // Required for Shift+Tab
+	
+	public InsertStringGestureData(String string, boolean shiftDown)
+	{
+		super();
+		_string = string;
+		_shiftDown = shiftDown;
+	}
+	
+	public InsertStringGestureData(char c, boolean shiftDown)
+	{
+		super();
+		_string = new String("" + c);
+		_shiftDown = shiftDown;
+	} 
+	
+	public InsertStringGestureData(InsertStringGestureData other)
+	{
+		super(other);
+		_string = new String(other._string);
+		_shiftDown = other._shiftDown;
+	}
+	
+	public String getString()
+	{
+		return _string;
+	}
+	
+	public boolean isShiftDown()
+	{
+		return _shiftDown;
+	}
+	
+	@Override
+	public GestureData clone()
+	{
+		return new InsertStringGestureData(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/ItemSpecificGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/ItemSpecificGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/ItemSpecificGestureData.java	(revision 1097)
@@ -0,0 +1,85 @@
+package org.expeditee.gio.gesture.data;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+import org.expeditee.core.Point;
+import org.expeditee.gui.FrameUtils;
+import org.expeditee.items.Item;
+import org.expeditee.items.Text;
+
+public class ItemSpecificGestureData extends GestureData {
+
+	protected Item _currentItem;
+	protected Collection<Item> _currentItems;
+	protected Collection<Item> _enclosure;
+	
+	public ItemSpecificGestureData(Item item)
+	{
+		super();
+		_currentItem = item;
+	}
+	
+	public ItemSpecificGestureData()
+	{
+		super();
+		init();
+	}
+	
+	public ItemSpecificGestureData(Point position)
+	{
+		super(position);
+		init();
+	}
+	
+	public ItemSpecificGestureData(ItemSpecificGestureData other)
+	{
+		super(other);
+		if (other != null) {
+			_currentItem = other._currentItem;
+			if (other._currentItems != null) {
+				_currentItems = new LinkedHashSet<Item>(other._currentItems.size());
+				_currentItems.addAll(other._currentItems);
+			}
+			if (other._enclosure != null) {
+				_enclosure = new LinkedHashSet<Item>(other._enclosure.size());
+				_enclosure.addAll(other._enclosure);
+			}
+		}
+	}
+	
+	public Item getCurrentItem()
+	{
+		return _currentItem;
+	}
+	
+	public Text getCurrentTextItem()
+	{
+		if (_currentItem instanceof Text) return (Text) _currentItem;
+		
+		return null;
+	}
+	
+	public Collection<Item> getCurrentItems()
+	{
+		return _currentItems;
+	}
+	
+	public Collection<Item> getEnclosure()
+	{
+		return _enclosure;
+	}
+	
+	private void init()
+	{
+		_currentItem = FrameUtils.getCurrentItem();
+		_currentItems = FrameUtils.getCurrentItems(_currentItem);
+		_enclosure = FrameUtils.getEnclosingLineEnds();
+	}
+	
+	@Override
+	public GestureData clone()
+	{
+		return new ItemSpecificGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/JustifyGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/JustifyGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/JustifyGestureData.java	(revision 1097)
@@ -0,0 +1,46 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.gui.DisplayController;
+import org.expeditee.gui.Frame;
+
+public class JustifyGestureData extends ItemSpecificGestureData {
+
+	private Frame _frame;
+	private boolean _resetWidth;
+	
+	public JustifyGestureData(boolean resetWidth)
+	{
+		super();
+		_frame = DisplayController.getCurrentFrame();
+		_resetWidth = resetWidth;
+	}
+	
+	public JustifyGestureData(Frame frame, boolean resetWidth)
+	{
+		super();
+		_frame = frame;
+		_resetWidth = resetWidth;
+	}
+	
+	public JustifyGestureData(JustifyGestureData other)
+	{
+		super(other);
+		_frame = other._frame;
+		_resetWidth = other._resetWidth;
+	}
+	
+	public Frame getFrame()
+	{
+		return _frame;
+	}
+	
+	public boolean getResetWidth()
+	{
+		return _resetWidth;
+	}
+	
+	public JustifyGestureData clone()
+	{
+		return new JustifyGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/LinkGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/LinkGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/LinkGestureData.java	(revision 1097)
@@ -0,0 +1,29 @@
+package org.expeditee.gio.gesture.data;
+
+public class LinkGestureData extends ItemSpecificGestureData {
+
+	private boolean _follow;
+	
+	public LinkGestureData(boolean follow)
+	{
+		super();
+		_follow = follow;
+	}
+	
+	public LinkGestureData(LinkGestureData other)
+	{
+		super(other);
+		_follow = other._follow;
+	}
+	
+	public boolean getFollow()
+	{
+		return _follow;
+	}
+	
+	public LinkGestureData clone()
+	{
+		return new LinkGestureData(this);
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/NavigateFrameGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/NavigateFrameGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/NavigateFrameGestureData.java	(revision 1097)
@@ -0,0 +1,68 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.gui.DisplayController;
+
+public class NavigateFrameGestureData extends GestureData {
+	
+	public enum NavigateTo {
+		ZERO_FRAME, // The zero frame for the current frameset
+		FIRST_FRAME, // The one frame for the current frameset
+		LAST_FRAME, // The last frame in the current frameset
+		NEXT_FRAME, // The (n+1) frame in the current frameset
+		PREVIOUS_FRAME, // The (n-1) frame in the current frameset
+		FORWARD_FRAME, // Undoes back
+		BACK_FRAME, // The previous frame in your history
+		EARLIEST_FRAME, // Goes back to the beginning of your history
+		LATEST_FRAME, // Goes forward to the end of your history
+		SPECIFIC_FRAME // A named frame
+	}
+	
+	private String _currentFrameset;
+	private int _currentFrameNumber;
+	private NavigateTo _navigateTo;
+	
+	public NavigateFrameGestureData(NavigateTo navigateTo)
+	{
+		super();
+		_currentFrameset = DisplayController.getCurrentFrame().getFramesetName();
+		_currentFrameNumber = DisplayController.getCurrentFrame().getNumber();
+		_navigateTo = navigateTo;
+	}
+	
+	public NavigateFrameGestureData(String frameset, int frameNumber)
+	{
+		super();
+		_currentFrameset = frameset;
+		_currentFrameNumber = frameNumber;
+		_navigateTo = NavigateTo.SPECIFIC_FRAME;
+	}
+	
+	public NavigateFrameGestureData(NavigateFrameGestureData other)
+	{
+		super(other);
+		_currentFrameset = other._currentFrameset;
+		_currentFrameNumber = other._currentFrameNumber;
+		_navigateTo = other._navigateTo;
+		
+	}
+	
+	public String getCurrentFrameset()
+	{
+		return _currentFrameset;
+	}
+	
+	public int getCurrentFrameNumber()
+	{
+		return _currentFrameNumber;
+	}
+	
+	public NavigateTo getNavigateTo()
+	{
+		return _navigateTo;
+	}
+	
+	public NavigateFrameGestureData clone()
+	{
+		return new NavigateFrameGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/NavigateTextGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/NavigateTextGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/NavigateTextGestureData.java	(revision 1097)
@@ -0,0 +1,52 @@
+package org.expeditee.gio.gesture.data;
+
+public class NavigateTextGestureData extends ItemSpecificGestureData {
+
+	public enum NavigateTo {
+		PREVIOUS_CHARACTER,
+		NEXT_CHARACTER,
+		PREVIOUS_WORD,
+		NEXT_WORD,
+		ABOVE_LINE,
+		BELOW_LINE,
+		LINE_END,
+		LINE_HOME,
+		PARAGRAPH_END,
+		PARAGRAPH_HOME,
+		NEXT_TEXT_ITEM,
+		PREVIOUS_TEXT_ITEM
+	}
+	
+	private NavigateTo _navigateTo;
+	private boolean _selecting;
+	
+	public NavigateTextGestureData(NavigateTo navigateTo, boolean selecting)
+	{
+		super();
+		_navigateTo = navigateTo;
+		_selecting = selecting;
+	}
+	
+	public NavigateTextGestureData(NavigateTextGestureData other)
+	{
+		super(other);
+		_navigateTo = other._navigateTo;
+		_selecting = other._selecting;
+	}
+	
+	public NavigateTo getNavigateTo()
+	{
+		return _navigateTo;
+	}
+	
+	public boolean getSelecting()
+	{
+		return _selecting;
+	}
+	
+	@Override
+	public GestureData clone()
+	{
+		return new NavigateTextGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/PanGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/PanGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/PanGestureData.java	(revision 1097)
@@ -0,0 +1,46 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.core.Point;
+
+/**
+ * Extra data provided with PAN gestures.
+ * 
+ * @author cts16
+ *
+ */
+public class PanGestureData extends GestureData {
+
+	/** The position where this PAN gesture started. */
+	private Point _panStart;
+	/** The distance to pan through. */
+	private Point _panDelta;
+	
+	public PanGestureData(Point position, Point startPosition, Point deltaPosition)
+	{
+		super(position);
+		_panStart = startPosition.clone();
+		_panDelta = deltaPosition.clone();
+	}
+	
+	public PanGestureData(PanGestureData other) {
+		super(other);
+		_panStart = other._panStart.clone();
+		_panDelta = other._panDelta.clone();
+	}
+	
+	public Point getPanStart()
+	{
+		return _panStart.clone();
+	}
+	
+	public Point getPanDelta()
+	{
+		return _panDelta.clone();
+	}
+
+	@Override
+	public GestureData clone() {
+		return new PanGestureData(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/PickUpGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/PickUpGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/PickUpGestureData.java	(revision 1097)
@@ -0,0 +1,66 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.core.Point;
+
+public class PickUpGestureData extends SelectAreaGestureData {
+
+	protected boolean _copy;
+	protected boolean _extrude;
+	protected boolean _inheritAttributes;
+	
+	public PickUpGestureData(Point moveTo, Point draggedFrom, boolean copy, boolean inheritAttributes)
+	{
+		super(moveTo, draggedFrom, false);
+		_copy = copy;
+		_extrude = false;
+		_inheritAttributes = inheritAttributes;
+	}
+	
+	public PickUpGestureData(boolean copy, boolean extrude, boolean inheritAttributes)
+	{
+		super();
+		_copy = copy;
+		_extrude = extrude;
+		_inheritAttributes = inheritAttributes;
+	}
+	
+	public PickUpGestureData(boolean copy, boolean extrude)
+	{
+		this(copy, extrude, false);
+	}
+	
+	public PickUpGestureData(boolean copy)
+	{
+		this(copy, false);
+	}
+	
+	public PickUpGestureData(PickUpGestureData other)
+	{
+		super(other);
+		if (other != null) {
+			_copy = other._copy;
+			_extrude = other._extrude;
+			_inheritAttributes = other._inheritAttributes;
+		}
+	}
+	
+	public boolean getCopy()
+	{
+		return _copy;
+	}
+	
+	public boolean getExtrude()
+	{
+		return _extrude;
+	}
+	
+	public boolean getInheritAttributes()
+	{
+		return _inheritAttributes;
+	}
+	
+	public PickUpGestureData clone()
+	{
+		return new PickUpGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/RefreshGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/RefreshGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/RefreshGestureData.java	(revision 1097)
@@ -0,0 +1,36 @@
+package org.expeditee.gio.gesture.data;
+
+public class RefreshGestureData extends GestureData {
+	
+	private boolean _shouldReloadFrameFirst;
+	private boolean _shouldRefreshFrameSize;
+	
+	public RefreshGestureData(boolean shouldReloadFrameFirst, boolean shouldRefreshFrameSize)
+	{
+		super();
+		_shouldReloadFrameFirst = shouldReloadFrameFirst;
+		_shouldRefreshFrameSize = shouldRefreshFrameSize;
+	}
+
+	public RefreshGestureData(RefreshGestureData other)
+	{
+		super(other);
+		_shouldReloadFrameFirst = other._shouldReloadFrameFirst;
+		_shouldRefreshFrameSize = other._shouldRefreshFrameSize;
+	}
+	
+	public boolean shouldReloadFrameFirst()
+	{
+		return _shouldReloadFrameFirst;
+	}
+	
+	public boolean shouldRefreshFrameSize()
+	{
+		return _shouldRefreshFrameSize;
+	}
+	
+	public RefreshGestureData clone()
+	{
+		return new RefreshGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/ScaleGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/ScaleGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/ScaleGestureData.java	(revision 1097)
@@ -0,0 +1,50 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.core.Point;
+
+/**
+ * Gesture data for scale gesture.
+ * 
+ * @author cts16
+ */
+public class ScaleGestureData extends ItemSpecificGestureData {
+	
+	private int _scaleFactor;
+	
+	public ScaleGestureData(int scaleFactor)
+	{
+		super();
+		_scaleFactor = scaleFactor;		
+	}
+	
+	/**
+	 * @param centreOfScale
+	 * 			Position to scale outward from. Should be
+	 * 			null to scale relative to item.
+	 * 
+	 * @param scaleFactor
+	 */
+	public ScaleGestureData(Point centreOfScale, int scaleFactor)
+	{
+		super(centreOfScale);
+		_scaleFactor = scaleFactor;
+	}
+	
+	public ScaleGestureData(ScaleGestureData other)
+	{
+		super(other);
+		_scaleFactor = other._scaleFactor;
+	}
+	
+	public int getScaleFactor()
+	{
+		return _scaleFactor;
+	}
+	
+	@Override
+	public GestureData clone()
+	{
+		return new ScaleGestureData(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/SelectAreaGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/SelectAreaGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/SelectAreaGestureData.java	(revision 1097)
@@ -0,0 +1,78 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.core.Point;
+
+public class SelectAreaGestureData extends ItemSpecificGestureData {
+
+	private Point _draggedFrom;
+	private boolean _finishedDragging;
+	
+	public SelectAreaGestureData(Point moveTo, Point draggedFrom, boolean finishedDragging)
+	{
+		super(moveTo);
+		if (draggedFrom != null) _draggedFrom = draggedFrom.clone();
+		_finishedDragging = finishedDragging;
+	}
+	
+	public SelectAreaGestureData(Point moveTo, Point draggedFrom)
+	{
+		this(moveTo, draggedFrom, false);
+	}
+	
+	public SelectAreaGestureData(Point moveTo)
+	{
+		this(moveTo, null, false);
+	}
+	
+	public SelectAreaGestureData()
+	{
+		super();
+		_draggedFrom = null;
+		_finishedDragging = true;
+	}
+	
+	public SelectAreaGestureData(SelectAreaGestureData other)
+	{
+		super(other);
+		if (other != null) {
+			if (other._draggedFrom != null) _draggedFrom = other._draggedFrom.clone();
+			_finishedDragging = other._finishedDragging;
+		}
+	}
+	
+	public SelectAreaGestureData carryOn(Point moveTo)
+	{
+		SelectAreaGestureData data = this.clone();
+		data._position = moveTo;
+		return data;
+	}
+	
+	public SelectAreaGestureData finalise()
+	{
+		SelectAreaGestureData data = this.clone();
+		data._finishedDragging = true;
+		return data;
+	}
+	
+	public boolean wasDragged()
+	{
+		return _draggedFrom != null;
+	}
+	
+	public Point getDraggedFrom()
+	{
+		if (_draggedFrom == null) return null;
+		
+		return _draggedFrom.clone();
+	}
+	
+	public boolean getFinishedDragging()
+	{
+		return _finishedDragging;
+	}
+	
+	public SelectAreaGestureData clone()
+	{
+		return new SelectAreaGestureData(this);
+	}
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/UndoGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/UndoGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/UndoGestureData.java	(revision 1097)
@@ -0,0 +1,29 @@
+package org.expeditee.gio.gesture.data;
+
+public class UndoGestureData extends GestureData {
+	
+	private boolean _redo;
+	
+	public UndoGestureData(boolean redo)
+	{
+		super();
+		_redo = redo;
+	}
+	
+	public UndoGestureData(UndoGestureData other)
+	{
+		super(other);
+		_redo = other._redo;
+	}
+	
+	public boolean getRedo()
+	{
+		return _redo;
+	}
+	
+	public UndoGestureData clone()
+	{
+		return new UndoGestureData(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/gesture/data/ZoomGestureData.java
===================================================================
--- /trunk/src/org/expeditee/gio/gesture/data/ZoomGestureData.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/gesture/data/ZoomGestureData.java	(revision 1097)
@@ -0,0 +1,31 @@
+package org.expeditee.gio.gesture.data;
+
+import org.expeditee.core.Point;
+
+public class ZoomGestureData extends GestureData {
+
+	private float _scaleFactor;
+	
+	public ZoomGestureData(Point centreOfZoom, float scaleFactor)
+	{
+		super(centreOfZoom);
+		_scaleFactor = scaleFactor;
+	}
+	
+	public ZoomGestureData(ZoomGestureData other) {
+		super(other);
+		_scaleFactor = other._scaleFactor;
+	}
+	
+	public float getScaleFactor()
+	{
+		return _scaleFactor;
+	}
+	
+	@Override
+	public GestureData clone()
+	{
+		return new ZoomGestureData(this);
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/input/InputEvent.java
===================================================================
--- /trunk/src/org/expeditee/gio/input/InputEvent.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/input/InputEvent.java	(revision 1097)
@@ -0,0 +1,33 @@
+package org.expeditee.gio.input;
+
+/**
+ * The base class for all input event types.
+ * 
+ * @author cts16
+ */
+public class InputEvent {
+
+	/** The possible types of input Expeditee deals with. */
+	public enum InputType {
+		KBM,
+		TIMEOUT,
+		TOUCH,
+		VOICE,
+		ETC
+	}
+	
+	/** The type of input this event came from. */
+	private InputType _inputType;
+	
+	/** Standard constructor must provide the type of input event. */
+	protected InputEvent(InputType type)
+	{
+		_inputType = type;
+	}
+	
+	/** Returns the input type for this event. */
+	public InputType getInputType()
+	{
+		return _inputType;
+	}
+}
Index: /trunk/src/org/expeditee/gio/input/InputEventListener.java
===================================================================
--- /trunk/src/org/expeditee/gio/input/InputEventListener.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/input/InputEventListener.java	(revision 1097)
@@ -0,0 +1,10 @@
+package org.expeditee.gio.input;
+
+public interface InputEventListener {
+	
+	/**
+	 * @return True to consume the event, false to let it pass.
+	 */
+	public boolean onInputEvent(InputEvent event);
+
+}
Index: /trunk/src/org/expeditee/gio/input/InputEventToGestureTranslator.java
===================================================================
--- /trunk/src/org/expeditee/gio/input/InputEventToGestureTranslator.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/input/InputEventToGestureTranslator.java	(revision 1097)
@@ -0,0 +1,52 @@
+package org.expeditee.gio.input;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.expeditee.Util;
+import org.expeditee.gio.gesture.Gesture;
+import org.expeditee.gio.input.InputEvent.InputType;
+
+public abstract class InputEventToGestureTranslator
+{
+	/** The set of monitored input types. */
+	protected HashSet<InputType> _monitoredInputTypes = new HashSet<InputType>();
+	
+	/** Default constructor. */
+	protected InputEventToGestureTranslator()
+	{
+	}
+	
+	/** Constructor tells which input type this translator monitors. */
+	public InputEventToGestureTranslator(InputType monitoredInputType)
+	{
+		if (monitoredInputType != null) _monitoredInputTypes.add(monitoredInputType);
+	}
+	
+	/** Constructor tells which input types this translator monitors. */
+	public InputEventToGestureTranslator(InputType[] monitoredInputTypes)
+	{
+		_monitoredInputTypes = Util.hashSetFromArray(monitoredInputTypes);
+	}
+	
+	/** Returns a string describing this gesture. */
+	public abstract String details();
+	
+	/** Method which receives input events and translates them into Expeditee gestures. */
+	public abstract List<Gesture> onInputEvent(InputEvent event);
+	
+	/** Gets the set of all input types that this translator monitors. */
+	public Set<InputType> getMonitoredInputTypes()
+	{
+		return new HashSet<InputType>(_monitoredInputTypes);
+	}
+	
+	/** Finds out if the given input type is monitored by this translator. */
+	public boolean isMonitoredType(InputType type)
+	{
+		if (_monitoredInputTypes == null) return false;
+		return _monitoredInputTypes.contains(type);
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/input/InputEventToGestureTranslatorSet.java
===================================================================
--- /trunk/src/org/expeditee/gio/input/InputEventToGestureTranslatorSet.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/input/InputEventToGestureTranslatorSet.java	(revision 1097)
@@ -0,0 +1,76 @@
+package org.expeditee.gio.input;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.expeditee.gio.gesture.Gesture;
+import org.expeditee.gio.input.InputEvent.InputType;
+
+/**
+ * An input-event -> gesture translator that is the aggregation of a set of other
+ * translators.
+ * 
+ * @author cts16
+ */
+public class InputEventToGestureTranslatorSet extends InputEventToGestureTranslator
+{
+	/** The individual gesture recognisers. */
+	private List<InputEventToGestureTranslator> _recognisers;
+	
+	public InputEventToGestureTranslatorSet()
+	{
+		_recognisers = new LinkedList<InputEventToGestureTranslator>();
+	}
+	
+	/** Adds a sub-translator to this translator set. */
+	public void addTranslator(InputEventToGestureTranslator translator)
+	{
+		if (translator == null) return;
+		
+		_recognisers.add(translator);
+
+		Set<InputType> recogniserMonitoredTypes = translator.getMonitoredInputTypes();
+		if (recogniserMonitoredTypes != null) _monitoredInputTypes.addAll(recogniserMonitoredTypes);
+	}
+	
+	@Override
+	public String details()
+	{
+		if (_recognisers.isEmpty()) return "";
+		
+		// Aggregate the details of the individual gesture recognisers
+		String details = "";
+		for (InputEventToGestureTranslator recogniser : _recognisers) {
+			String recogniserDetails = recogniser.details();
+			if (recogniserDetails != null && recogniserDetails != "") {
+				if (details != "") details += "\n";
+				details += recogniserDetails; 
+			}
+		}
+		
+		return details;
+	}
+	
+	@Override
+	public List<Gesture> onInputEvent(InputEvent event)
+	{
+		if (_recognisers.isEmpty()) return null;
+		
+		// Only interested in monitored events
+		if (event == null || !isMonitoredType(event.getInputType())) return null;
+		
+		// Aggregate the responses of all individual gesture listeners
+		List<Gesture> gestures = new LinkedList<Gesture>();
+		for (InputEventToGestureTranslator recogniser : _recognisers) {
+			if (recogniser.isMonitoredType(event.getInputType())) {
+				List<Gesture> recogniserGestures = recogniser.onInputEvent(event);
+				if (recogniserGestures != null) {
+					gestures.addAll(recogniserGestures);
+				}
+			}
+		}
+		
+		return gestures;
+	}
+}
Index: /trunk/src/org/expeditee/gio/input/KBMInputEvent.java
===================================================================
--- /trunk/src/org/expeditee/gio/input/KBMInputEvent.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/input/KBMInputEvent.java	(revision 1097)
@@ -0,0 +1,154 @@
+package org.expeditee.gio.input;
+
+import org.expeditee.core.Line;
+
+public class KBMInputEvent extends InputEvent {
+	
+	public enum EventType {
+		MOUSE_BUTTON_DOWN,
+		MOUSE_BUTTON_UP,
+		MOUSE_MOVE,
+		MOUSE_WHEEL_SCROLL,
+		KEY_DOWN,
+		KEY_UP,
+		CHAR_TYPED
+	}
+	
+	/**
+	 * Enumerates the mouse buttons of interest to Expeditee.
+	 */
+	public enum MouseButton {
+		LEFT,
+		MIDDLE,
+		RIGHT
+	}
+	
+	/**
+	 * Enumerates the keyboard keys of interest to Expeditee.
+	 */
+	public enum Key {
+		SHIFT,CTRL,ALT,
+		ESC,
+		F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12,
+		UP_ARROW,DOWN_ARROW,LEFT_ARROW,RIGHT_ARROW,
+		HOME,END,
+		DELETE,
+		PAGE_UP,PAGE_DOWN,
+		BACKSPACE,
+		TAB,
+		A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z
+	}
+	
+	private EventType _eventType;
+	private Object _eventData;
+	
+	public KBMInputEvent(EventType type, Object data) throws Exception
+	{
+		super(InputEvent.InputType.KBM);
+		_eventType = type;
+		_eventData = data;
+		checkData();
+	}
+	
+	public EventType getEventType()
+	{
+		return _eventType;
+	}
+	
+	public Object getData()
+	{
+		return _eventData;
+	}
+	
+	/** Gets the key that went down if this is a key-down event. */
+	public Key getKeyDown()
+	{
+		if (_eventType == EventType.KEY_DOWN) return (Key) _eventData;
+		
+		return null;
+	}
+	
+	/** Gets the key that went up if this is a key-up event. */
+	public Key getKeyUp()
+	{
+		if (_eventType == EventType.KEY_UP) return (Key) _eventData;
+		
+		return null;
+	}
+	
+	/**
+	 * Gets the name of the key that was pressed/released.
+	 * Returns the empty string if this is not a key event.
+	 */
+	public String getKeyText()
+	{
+		Key key = getKeyDown();
+		if (key == null) key = getKeyUp();
+		if (key == null) return "";
+		return key.name();
+	}
+	
+	/** Gets the character that was typed if this is a CHAR_TYPED event. */
+	public Character getCharTyped()
+	{
+		if (_eventType == EventType.CHAR_TYPED) return (Character) _eventData;
+		
+		return null;
+	}
+	
+	/** Gets the mouse button that went down if this is a mouse-button-down event. */
+	public MouseButton getMouseButtonDown()
+	{
+		if (_eventType == EventType.MOUSE_BUTTON_DOWN) return (MouseButton) _eventData;
+		
+		return null;
+	}
+	
+	/** Gets the mouse button that went up if this is a mouse-button-up event. */
+	public MouseButton getMouseButtonUp()
+	{
+		if (_eventType == EventType.MOUSE_BUTTON_UP) return (MouseButton) _eventData;
+		
+		return null;
+	}
+	
+	/** Gets the position of the mouse if this is a mouse-move event. */
+	public Line getMouseMove()
+	{
+		if (_eventType == EventType.MOUSE_MOVE) return (Line) _eventData;
+		
+		return null;
+	}
+	
+	private void checkData() throws Exception
+	{
+		if (_eventType == null) throw new Exception("No event type specified");
+		if (_eventData == null) throw new Exception("No event data specified");
+		
+		switch (_eventType) {
+		case MOUSE_BUTTON_DOWN:
+		case MOUSE_BUTTON_UP:
+			if (!(_eventData instanceof MouseButton)) throw new Exception("MouseButton data expected");
+			break;
+		case KEY_DOWN:
+		case KEY_UP:
+			if (!(_eventData instanceof Key)) throw new Exception("Key data expected");
+			break;
+		case CHAR_TYPED:
+			if (!(_eventData instanceof Character)) throw new Exception("Character data expected");
+			break;
+		case MOUSE_MOVE:
+			if (!(_eventData instanceof Line)) throw new Exception("Line data expected");
+			break;
+		case MOUSE_WHEEL_SCROLL:
+			if (!(_eventData instanceof Integer)) throw new Exception("Integer data expected");
+			break;
+		}
+	}
+
+	@Override
+	public String toString()
+	{
+		return _eventType.toString() + " event : Data = " + _eventData.toString();
+	}
+}
Index: /trunk/src/org/expeditee/gio/input/StandardInputEventListeners.java
===================================================================
--- /trunk/src/org/expeditee/gio/input/StandardInputEventListeners.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/input/StandardInputEventListeners.java	(revision 1097)
@@ -0,0 +1,207 @@
+package org.expeditee.gio.input;
+
+import org.expeditee.actions.Actions;
+import org.expeditee.actions.Simple;
+import org.expeditee.gio.InputManager;
+import org.expeditee.gio.input.InputEvent.InputType;
+import org.expeditee.gio.input.KBMInputEvent.EventType;
+import org.expeditee.gio.input.KBMInputEvent.Key;
+import org.expeditee.gio.input.KBMInputEvent.MouseButton;
+import org.expeditee.gui.FrameUtils;
+import org.expeditee.gui.Help;
+import org.expeditee.stats.Logger;
+import org.expeditee.stats.SessionStats;
+
+/**
+ * Static class to house all of the standard Expeditee input listeners.
+ * 
+ * @author cts16
+ */
+public class StandardInputEventListeners {
+
+	/** Cannot be constructed, static class only. */
+	private StandardInputEventListeners()
+	{
+	}
+	
+	/** Registers the standard input listeners with the given input manager. */
+	public static void registerWith(InputManager manager)
+	{
+		if (manager == null) return;
+		
+		// Register the listeners in priority order
+		manager.registerInputEventListener(kbmStateListener);
+		manager.registerInputEventListener(helpListener);
+		manager.registerInputEventListener(statisticsListener);
+		manager.registerInputEventListener(agentListener);
+		manager.registerInputEventListener(simpleListener);
+	}
+	
+	/*
+	 * 
+	 * Expeditee's standard input-event listeners.
+	 * 
+	 */
+	
+	/** An input listener to track the state of all keys and mouse buttons. */
+	public static final KBMStateListener kbmStateListener = new KBMStateListener();
+	public static class KBMStateListener implements InputEventListener
+	{
+		private Boolean[] _keyState = new Boolean[Key.values().length];
+		private Boolean[] _mouseState = new Boolean[MouseButton.values().length];
+		
+		@Override
+		public boolean onInputEvent(InputEvent event)
+		{
+			// Only interested in keyboard/mouse events
+			if (event.getInputType() != InputType.KBM) return false;
+			
+			// Cast the event
+			KBMInputEvent kbmEvent = (KBMInputEvent) event;
+			
+			// Track any key state changes
+			if (kbmEvent.getKeyDown() != null) {
+				_keyState[kbmEvent.getKeyDown().ordinal()] = true;
+			} else if (kbmEvent.getKeyUp() != null) {
+				_keyState[kbmEvent.getKeyUp().ordinal()] = false;
+			}
+			
+			// Track any mouse button state changes
+			if (kbmEvent.getMouseButtonDown() != null) {
+				_mouseState[kbmEvent.getMouseButtonDown().ordinal()] = true;
+			} else if (kbmEvent.getMouseButtonUp() != null) {
+				_mouseState[kbmEvent.getMouseButtonUp().ordinal()] = false;
+			}
+			
+			return false;
+		}
+		
+		public boolean isKeyDown(Key key)
+		{
+			return (key != null) && (_keyState[key.ordinal()] == Boolean.TRUE);
+		}
+		
+		public boolean isMouseButtonDown(MouseButton button)
+		{
+			return (button != null) && (_mouseState[button.ordinal()] == Boolean.TRUE);
+		}
+		
+		public boolean isOnlyMouseButtonDown(MouseButton button)
+		{
+			if (button == null) return false;
+			
+			for (MouseButton b : MouseButton.values()) {
+				if ((b == button && !isMouseButtonDown(b)) || (b != button && isMouseButtonDown(b))) return false;
+			}
+			
+			return true;
+		}
+		
+		public boolean isAnyMouseButtonDown()
+		{
+			for (MouseButton button : MouseButton.values()) {
+				if (isMouseButtonDown(button)) return true;
+			}
+			
+			return false;
+		}
+	};
+	
+	/** An input listener to monitor the input for statistical purposes. */ 
+	private static InputEventListener statisticsListener = new InputEventListener()
+	{
+		@Override
+		public boolean onInputEvent(InputEvent event) {
+			// Only interested in keyboard/mouse events
+			if (event.getInputType() != InputEvent.InputType.KBM) return false;
+			KBMInputEvent kbmEvent = (KBMInputEvent) event;
+			if (kbmEvent.getEventType() == EventType.KEY_DOWN) {
+				// Notify the change in stats
+				// TODO: Will changing this to the Expeditee KBMInputEvent upset stats? cts16
+				SessionStats.AddFrameEvent("k" + kbmEvent.getKeyText());
+
+				// Used for calculating frame stats (response time)
+				FrameUtils.ResponseTimer.restart();
+				
+				// Watch for escape key
+				if (kbmEvent.getKeyDown() == Key.ESC) SessionStats.Escape();
+				
+				if (kbmStateListener.isKeyDown(Key.CTRL)) {
+					Logger.Log(Logger.USER, Logger.CONTROL_CHAR, "User pressing: Ctrl+" + kbmEvent.getKeyText());
+				}
+			} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN) {
+				SessionStats.MouseClicked(kbmEvent.getMouseButtonDown());
+				switch (kbmEvent.getMouseButtonDown()) {
+				case LEFT:
+					SessionStats.AddFrameEvent("Ld");
+					break;
+				case MIDDLE:
+					SessionStats.AddFrameEvent("Md");
+					break;
+				case RIGHT:
+					SessionStats.AddFrameEvent("Rd");
+					break;
+				default:
+					break;
+				}
+			} else if (kbmEvent.getEventType() == EventType.MOUSE_BUTTON_UP) {
+				FrameUtils.ResponseTimer.restart();
+				switch (kbmEvent.getMouseButtonUp()) {
+				case LEFT:
+					SessionStats.AddFrameEvent("Lu");
+					break;
+				case MIDDLE:
+					SessionStats.AddFrameEvent("Mu");
+					break;
+				case RIGHT:
+					SessionStats.AddFrameEvent("Ru");
+					break;
+				default:
+					break;
+				}
+			}
+			return false;
+		}
+	};
+				
+	/** An input listener to monitor keyboard input for running agents. */
+	private static InputEventListener agentListener = new InputEventListener() {
+		@Override
+		public boolean onInputEvent(InputEvent event) {
+			// Only interested in keyboard/mouse events
+			if (event.getInputType() != InputType.KBM) return false;
+			KBMInputEvent kbmEvent = (KBMInputEvent) event;
+			if (Actions.isAgentRunning()) {
+				if (kbmEvent.getKeyDown() == Key.ESC || kbmEvent.getEventType() == EventType.MOUSE_BUTTON_DOWN)	{
+					Actions.stopAgent();
+				} else if (kbmEvent.getEventType() == EventType.KEY_DOWN) {
+					Actions.interruptAgent();
+				}
+			}
+			return false;
+		}
+	};
+				
+	/** An input listener to consume keyboard input for running Simple programs. */
+	private static InputEventListener simpleListener = new InputEventListener() {
+		@Override
+		public boolean onInputEvent(InputEvent event) {
+			// Only interested in keyboard/mouse events
+			if (event.getInputType() != InputEvent.InputType.KBM) return false;
+			if (Simple.consumeKeyboardInput()) return true;
+			return false;
+		}
+	};
+	
+	/** An input listener to update the message-bay help text. */
+	private static InputEventListener helpListener = new InputEventListener()
+	{
+		@Override
+		public boolean onInputEvent(InputEvent event) {
+			// Only interested in keyboard/mouse events
+			if (event.getInputType() != InputEvent.InputType.KBM) return false;
+			Help.updateStatus(kbmStateListener.isKeyDown(Key.CTRL), kbmStateListener.isKeyDown(Key.SHIFT));
+			return false;
+		}
+	};
+}
Index: /trunk/src/org/expeditee/gio/input/TimeoutInputEvent.java
===================================================================
--- /trunk/src/org/expeditee/gio/input/TimeoutInputEvent.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/input/TimeoutInputEvent.java	(revision 1097)
@@ -0,0 +1,29 @@
+package org.expeditee.gio.input;
+
+import org.expeditee.gio.TimeoutQueue.TimeoutHandle;
+
+/**
+ * Type of input event where the user can set a timer and
+ * receive an input event when the timer expires.
+ * 
+ * @author cts16
+ * 
+ * TODO: Revise above comment. cts16
+ * TODO: Complete. cts16
+ */
+public class TimeoutInputEvent extends InputEvent {
+
+	private TimeoutHandle _handle;
+	
+	public TimeoutInputEvent(TimeoutHandle handle)
+	{
+		super(InputEvent.InputType.TIMEOUT);
+		_handle = handle;
+	}
+	
+	public TimeoutHandle getHandle()
+	{
+		return _handle;
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXClipboardManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXClipboardManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXClipboardManager.java	(revision 1097)
@@ -0,0 +1,84 @@
+package org.expeditee.gio.javafx;
+
+import java.util.Set;
+
+import org.expeditee.core.BlockingRunnable;
+import org.expeditee.gio.ClipboardManager;
+import org.expeditee.gio.EcosystemManager;
+
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.DataFormat;
+
+public class JavaFXClipboardManager extends ClipboardManager {
+	
+	/** Data format for identifying Expeditee-specific data on the clipboard. */
+	private static final DataFormat expediteeDataFormat = new DataFormat("expediteeDataFormat");
+	
+	/** Singleton instance. */
+	private static JavaFXClipboardManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXClipboardManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXClipboardManager();
+		
+		return _instance;
+	}
+	
+	private Clipboard _clipboard;
+	
+	private JavaFXClipboardManager()
+	{
+		// Get a reference to the system clipboard
+		EcosystemManager.getMiscManager().runOnGIOThread(new BlockingRunnable() {
+			@Override
+			public void execute()
+			{
+				_clipboard = Clipboard.getSystemClipboard();
+			}
+		});
+	}
+
+	@Override
+	public void set(ClipboardData data)
+	{
+		// Leave the clipboard unchanged if data is null
+		if (data == null) return;
+		
+		// Fill out the data for the clipboard
+		ClipboardContent content = new ClipboardContent();
+		if (data.data != null) content.put(expediteeDataFormat, data.data);
+		if (data.imageRepresentation != null) {
+			javafx.scene.image.Image jfxImage = JavaFXMiscManager.getIfUsingJavaFXImageManager().getInternalImage(data.imageRepresentation);
+			content.putImage(jfxImage);
+		}
+		if (data.stringRepresentation != null) content.putString(data.stringRepresentation);
+		
+		// Set the clipboard contents
+		_clipboard.setContent(content);
+		
+	}
+
+	@Override
+	public ClipboardData get()
+	{
+		// Create an empty data structure for returning the gathered data
+		ClipboardData ret = new ClipboardData();
+		
+		// See which data formats are available for receipt
+		Set<DataFormat> availableFormats = _clipboard.getContentTypes();
+		
+		// Fill out the return structure
+		if (availableFormats.contains(expediteeDataFormat)) ret.data = _clipboard.getContent(expediteeDataFormat);
+		if (availableFormats.contains(DataFormat.IMAGE)) {
+			javafx.scene.image.Image jfxImage = _clipboard.getImage();
+			ret.imageRepresentation = JavaFXMiscManager.getIfUsingJavaFXImageManager().createImage(jfxImage);
+		}
+		if (availableFormats.contains(DataFormat.PLAIN_TEXT)) ret.stringRepresentation = _clipboard.getString();
+		
+		// Return it
+		return ret;
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXConversions.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXConversions.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXConversions.java	(revision 1097)
@@ -0,0 +1,142 @@
+package org.expeditee.gio.javafx;
+
+import javafx.scene.paint.CycleMethod;
+import javafx.scene.paint.Stop;
+
+public class JavaFXConversions {
+
+	public static org.expeditee.core.Colour fromJavaFXColor(javafx.scene.paint.Color color)
+	{
+		if (color == null) return null;
+		
+		return new org.expeditee.core.Colour((float) color.getRed(), (float) color.getGreen(), (float) color.getBlue(), (float) color.getOpacity());
+	}
+	
+	public static javafx.scene.paint.Color toJavaFXColor(org.expeditee.core.Colour colour)
+	{
+		if (colour == null) return null;
+		
+		return new javafx.scene.paint.Color(colour.getRedFloat(), colour.getGreenFloat(), colour.getBlueFloat(), colour.getAlphaFloat());
+	}
+	
+	public static org.expeditee.core.bounds.AxisAlignedBoxBounds fromJavaFXRectangle2D(javafx.geometry.Rectangle2D rectangle)
+	{
+		if (rectangle == null) return null;
+		
+		return new org.expeditee.core.bounds.AxisAlignedBoxBounds((int) rectangle.getMinX(), (int) rectangle.getMinY(), (int) rectangle.getWidth(), (int) rectangle.getHeight());
+	}
+	
+	public static javafx.geometry.Rectangle2D toJavaFXRectangle2D(org.expeditee.core.bounds.AxisAlignedBoxBounds box)
+	{
+		if (box == null) return null;
+		
+		return new javafx.geometry.Rectangle2D(box.getMinX(), box.getMinY(), box.getWidth(), box.getHeight());
+	}
+	
+	public static javafx.scene.paint.LinearGradient toJavaFXLinearGradient(org.expeditee.core.GradientFill fill)
+	{
+		if (fill == null) return null;
+		
+		Stop[] stops = new Stop[2];
+		stops[0] = new Stop(0, toJavaFXColor(fill.getFromColour()));
+		stops[1] = new Stop(1, toJavaFXColor(fill.getToColour()));
+		CycleMethod cyclic = fill.isAcyclic() ? CycleMethod.NO_CYCLE : CycleMethod.REFLECT;
+		return new javafx.scene.paint.LinearGradient(
+					fill.getFromPoint().x,
+					fill.getFromPoint().y,
+					fill.getToPoint().x,
+					fill.getToPoint().y,
+					false,
+					cyclic,
+					stops
+				);
+	}
+	
+	public static javafx.scene.shape.StrokeLineCap toJavaFXStrokeLineCap(org.expeditee.core.Stroke.CAP cap)
+	{
+		switch (cap) {
+		case BUTT:
+			return javafx.scene.shape.StrokeLineCap.BUTT;
+		case ROUND:
+			return javafx.scene.shape.StrokeLineCap.ROUND;
+		case SQUARE:
+			return javafx.scene.shape.StrokeLineCap.SQUARE;
+		default:
+			return null;
+		}
+	}
+	
+	public static javafx.scene.shape.StrokeLineJoin toJavaFXStrokeLineJoin(org.expeditee.core.Stroke.JOIN join)
+	{
+		switch (join) {
+		case BEVEL:
+			return javafx.scene.shape.StrokeLineJoin.BEVEL;
+		case MITER:
+			return javafx.scene.shape.StrokeLineJoin.MITER;
+		case ROUND:
+			return javafx.scene.shape.StrokeLineJoin.ROUND;
+		default:
+			return null;
+		}
+	}
+	
+	public static org.expeditee.gio.input.KBMInputEvent.Key fromJavaFXKeyCode(javafx.scene.input.KeyCode key)
+	{
+		switch (key) {
+		case SHIFT: return org.expeditee.gio.input.KBMInputEvent.Key.SHIFT;
+		case CONTROL: return org.expeditee.gio.input.KBMInputEvent.Key.CTRL;
+		case ALT: return org.expeditee.gio.input.KBMInputEvent.Key.ALT;
+		case ESCAPE: return org.expeditee.gio.input.KBMInputEvent.Key.ESC;
+		case F1: return org.expeditee.gio.input.KBMInputEvent.Key.F1;
+		case F2: return org.expeditee.gio.input.KBMInputEvent.Key.F2;
+		case F3: return org.expeditee.gio.input.KBMInputEvent.Key.F3;
+		case F4: return org.expeditee.gio.input.KBMInputEvent.Key.F4;
+		case F5: return org.expeditee.gio.input.KBMInputEvent.Key.F5;
+		case F6: return org.expeditee.gio.input.KBMInputEvent.Key.F6;
+		case F7: return org.expeditee.gio.input.KBMInputEvent.Key.F7;
+		case F8: return org.expeditee.gio.input.KBMInputEvent.Key.F8;
+		case F9: return org.expeditee.gio.input.KBMInputEvent.Key.F9;
+		case F10: return org.expeditee.gio.input.KBMInputEvent.Key.F10;
+		case F11: return org.expeditee.gio.input.KBMInputEvent.Key.F11;
+		case F12: return org.expeditee.gio.input.KBMInputEvent.Key.F12;
+		case UP: return org.expeditee.gio.input.KBMInputEvent.Key.UP_ARROW;
+		case DOWN: return org.expeditee.gio.input.KBMInputEvent.Key.DOWN_ARROW;
+		case LEFT: return org.expeditee.gio.input.KBMInputEvent.Key.LEFT_ARROW;
+		case RIGHT: return org.expeditee.gio.input.KBMInputEvent.Key.RIGHT_ARROW;
+		case HOME: return org.expeditee.gio.input.KBMInputEvent.Key.HOME;
+		case END: return org.expeditee.gio.input.KBMInputEvent.Key.END;
+		case DELETE: return org.expeditee.gio.input.KBMInputEvent.Key.DELETE;
+		case PAGE_UP: return org.expeditee.gio.input.KBMInputEvent.Key.PAGE_UP;
+		case PAGE_DOWN: return org.expeditee.gio.input.KBMInputEvent.Key.PAGE_DOWN;
+		case BACK_SPACE: return org.expeditee.gio.input.KBMInputEvent.Key.BACKSPACE;
+		case TAB: return org.expeditee.gio.input.KBMInputEvent.Key.TAB;
+		case A: return org.expeditee.gio.input.KBMInputEvent.Key.A;
+		case B: return org.expeditee.gio.input.KBMInputEvent.Key.B;
+		case C: return org.expeditee.gio.input.KBMInputEvent.Key.C;
+		case D: return org.expeditee.gio.input.KBMInputEvent.Key.D;
+		case E: return org.expeditee.gio.input.KBMInputEvent.Key.E;
+		case F: return org.expeditee.gio.input.KBMInputEvent.Key.F;
+		case G: return org.expeditee.gio.input.KBMInputEvent.Key.G;
+		case H: return org.expeditee.gio.input.KBMInputEvent.Key.H;
+		case I: return org.expeditee.gio.input.KBMInputEvent.Key.I;
+		case J: return org.expeditee.gio.input.KBMInputEvent.Key.J;
+		case K: return org.expeditee.gio.input.KBMInputEvent.Key.K;
+		case L: return org.expeditee.gio.input.KBMInputEvent.Key.L;
+		case M: return org.expeditee.gio.input.KBMInputEvent.Key.M;
+		case N: return org.expeditee.gio.input.KBMInputEvent.Key.N;
+		case O: return org.expeditee.gio.input.KBMInputEvent.Key.O;
+		case P: return org.expeditee.gio.input.KBMInputEvent.Key.P;
+		case Q: return org.expeditee.gio.input.KBMInputEvent.Key.Q;
+		case R: return org.expeditee.gio.input.KBMInputEvent.Key.R;
+		case S: return org.expeditee.gio.input.KBMInputEvent.Key.S;
+		case T: return org.expeditee.gio.input.KBMInputEvent.Key.T;
+		case U: return org.expeditee.gio.input.KBMInputEvent.Key.U;
+		case V: return org.expeditee.gio.input.KBMInputEvent.Key.V;
+		case W: return org.expeditee.gio.input.KBMInputEvent.Key.W;
+		case X: return org.expeditee.gio.input.KBMInputEvent.Key.X;
+		case Y: return org.expeditee.gio.input.KBMInputEvent.Key.Y;
+		case Z: return org.expeditee.gio.input.KBMInputEvent.Key.Z;
+		default: return null;
+		}
+	}
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXDragAndDropManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXDragAndDropManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXDragAndDropManager.java	(revision 1097)
@@ -0,0 +1,98 @@
+package org.expeditee.gio.javafx;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.expeditee.core.Point;
+import org.expeditee.gio.DragAndDropManager;
+import org.expeditee.gui.MessageBay;
+
+import javafx.event.EventHandler;
+import javafx.scene.Scene;
+import javafx.scene.input.DataFormat;
+import javafx.scene.input.DragEvent;
+import javafx.scene.input.Dragboard;
+import javafx.scene.input.TransferMode;
+
+public class JavaFXDragAndDropManager extends DragAndDropManager {
+	
+	/** Singleton instance. */
+	private static JavaFXDragAndDropManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXDragAndDropManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXDragAndDropManager();
+		
+		return _instance;
+	}
+
+	// GNOME and KDE desktops have a specialized way of DNDing files
+	private DataFormat _URIListDataflavorString = new DataFormat("text/uri-list;class=java.lang.String");
+	private DataFormat _URIListDataflavorCharArray = new DataFormat("text/uri-list;class=\"[C\"");
+	
+	private JavaFXDragAndDropManager()
+	{
+		Scene scene = JavaFXGraphicsManager.ExpediteeApplication.theScene;
+		
+		scene.setOnDragOver(new EventHandler<DragEvent>() {
+			@Override
+			public void handle(DragEvent event)
+			{
+				Dragboard dragboard = event.getDragboard();
+				if (	dragboard.hasString() ||
+						dragboard.hasFiles() ||
+						dragboard.hasContent(_URIListDataflavorString) ||
+						dragboard.hasContent(_URIListDataflavorCharArray))
+					
+				{
+					event.acceptTransferModes(TransferMode.COPY);
+				}
+				event.consume();
+			}
+		});
+		
+		scene.setOnDragDropped(new EventHandler<DragEvent>() {
+			@Override
+			public void handle(DragEvent event)
+			{
+				Dragboard dragboard = event.getDragboard();
+				Point dropPoint = new Point((int) event.getX(), (int) event.getY());
+				boolean success = false;
+				try {
+					
+					if (dragboard.hasFiles()) {
+						List<File> files = dragboard.getFiles();
+						if (files != null && files.size() > 0) {
+							importFileList(files, dropPoint);
+						}
+					} else if (dragboard.hasContent(_URIListDataflavorString)) {
+						String data = (String) dragboard.getContent(_URIListDataflavorString);
+						List<File> files = textURIListToFileList(data);
+						importFileList(files, dropPoint);
+						success = true;
+					} else if (dragboard.hasContent(_URIListDataflavorCharArray)) {
+						char[] data = (char[]) dragboard.getContent(_URIListDataflavorCharArray);
+						String uriString = new String(data);
+						List<File> files = textURIListToFileList(uriString);
+						importFileList(files, dropPoint);
+						success = true;
+					} else if (dragboard.hasString()) {
+						String str = dragboard.getString();
+						if (str != null && str.length() > 0) {
+							importString(str, dropPoint);
+							success = true;
+						}
+					}
+				} catch (IOException e) {
+					e.printStackTrace();
+					MessageBay.displayMessage("Failed to import data in Expeditee");
+				}
+				event.setDropCompleted(success);
+				event.consume();
+			}
+		});
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXEcosystemManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXEcosystemManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXEcosystemManager.java	(revision 1097)
@@ -0,0 +1,18 @@
+package org.expeditee.gio.javafx;
+
+import org.expeditee.gio.EcosystemManager;
+
+public class JavaFXEcosystemManager extends EcosystemManager {
+
+	public static void createJavaFXEcosystem()
+	{
+		EcosystemManager.setMiscManager(JavaFXMiscManager.getInstance());
+		EcosystemManager.setImageManager(JavaFXImageManager.getInstance());
+		EcosystemManager.setGraphicsManager(JavaFXGraphicsManager.getInstance());
+		EcosystemManager.setFontManager(JavaFXFontManager.getInstance());
+		EcosystemManager.setTextLayoutManager(JavaFXTextLayoutManager.getInstance());
+		EcosystemManager.setInputManager(JavaFXInputManager.getInstance());
+		EcosystemManager.setClipboardManager(JavaFXClipboardManager.getInstance());
+		EcosystemManager.setDragAndDropManager(JavaFXDragAndDropManager.getInstance());
+	}
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXFontManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXFontManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXFontManager.java	(revision 1097)
@@ -0,0 +1,246 @@
+package org.expeditee.gio.javafx;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import org.expeditee.core.Font;
+import org.expeditee.gio.FontManager;
+
+import javafx.scene.text.FontPosture;
+import javafx.scene.text.FontWeight;
+
+public class JavaFXFontManager extends FontManager {
+	
+	/** Singleton instance. */
+	private static JavaFXFontManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXFontManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXFontManager();
+		
+		return _instance;
+	}
+	
+	/** Mapping from handles to internal fonts. */
+	private HashMap<Font, JavaFXFont> _fontMap;
+	/** The font family names registered with the manager. */
+	private HashSet<String> _fontNames;
+	
+	/** Private constructor for singleton instance. */
+	private JavaFXFontManager()
+	{
+		// Initialise the font map
+		_fontMap = new HashMap<Font, JavaFXFont>();
+		
+		// Initialise the set of family names
+		_fontNames = new HashSet<String>();
+		
+		// Initialise default font to JavaFX default
+		setDefaultFont(register(JavaFXFont.getDefault()));
+		
+		// Add JavaFX standard fonts to registered fonts
+		registerJavaFXStandardFonts();
+	}
+
+	@Override
+	public Font registerFontFile(File fontFile)
+	{
+		if (fontFile == null) return null;
+		
+		JavaFXFont jfxFont = null;
+		boolean registerOkay = true;
+		
+		// Create the internal font from the given file
+		try {
+			jfxFont = JavaFXFont.loadFont(new FileInputStream(fontFile), 0); // Size of 0 forces default size
+			if (jfxFont == null) registerOkay = false;
+		} catch (Exception e) {
+			registerOkay = false;
+		}
+		
+		// Register the new font with ourselves
+		if (registerOkay) {
+			return register(jfxFont).clone();
+		}
+		
+		// If we made it here the registration failed
+		return null;
+	}
+
+	@Override
+	public String[] getRegisteredFontFamilies() {
+		
+		return _fontNames.toArray(new String[_fontNames.size()]);
+	}
+
+	@Override
+	public boolean isFontFamilyRegistered(String fontFamily)
+	{
+		return _fontNames.contains(fontFamily);
+	}
+
+	@Override
+	public Font getActualFont(Font font)
+	{
+		return extractFontForInternalFont(register(font));
+	}
+	
+	/** Registers the given handle with the manager and returns the internal font. */
+	private JavaFXFont register(Font font)
+	{
+		if (register(null, font) == null) return null;
+		
+		return _fontMap.get(font);
+	}
+
+	/** Registers the given font with the manager and returns the generated handle. */
+	private Font register(JavaFXFont jfxFont)
+	{
+		return register(jfxFont, null);
+	}
+	
+	/** Registers a handle/font pair. */
+	private Font register(JavaFXFont jfxFont, Font font)
+	{
+		if (jfxFont == null && font == null) return null;
+		
+		if (_fontMap.containsKey(font)) return font;
+		
+		if (font == null) {
+			font = extractFontForInternalFont(jfxFont);
+		} else if (jfxFont == null) {
+			jfxFont = JavaFXFont.font(font.getFamilyName(), getJavaFXFontWeight(font), getJavaFXFontPosture(font), font.getSize());
+		}
+		
+		if (jfxFont == null || font == null) return null;
+		
+		_fontMap.put(font, jfxFont);
+		_fontNames.add(jfxFont.getFamily());
+		
+		return font;
+	}
+	
+	/** Creates a handle to match the given JavaFX font. */
+	private Font extractFontForInternalFont(JavaFXFont jfxFont)
+	{
+		if (jfxFont == null) return null;
+		
+		Font font = new Font(jfxFont.getFamily());
+		font.setStyle(Font.Style.PLAIN);
+		if (jfxFont.isBold()) font.toggleBold();
+		if (jfxFont.isItalics()) font.toggleItalic();
+		font.setSize((int) jfxFont.getSize());
+		font.setSpacing(0); // Can't get tracking value for JavaFXFonts
+		
+		return font;
+	}
+	
+	/** Gets the internal JavaFX font object for use by other Swing managers. */
+	public javafx.scene.text.Font getInternalFont(Font font)
+	{
+		return register(font).getRealFont();
+	}
+	
+	/** Registers any standard fonts available on the system with the manager. */
+	private void registerJavaFXStandardFonts()
+	{
+		List<String> standardFontNames = javafx.scene.text.Font.getFamilies();
+		
+		for (String name : standardFontNames) {
+			register(new Font(name));
+		}
+	}
+	
+	/** Gets the JavaFX font weight for a given font. */
+	private javafx.scene.text.FontWeight getJavaFXFontWeight(Font font)
+	{
+		if (font.isBold()) {
+			return javafx.scene.text.FontWeight.BOLD;
+		} else {
+			return javafx.scene.text.FontWeight.NORMAL;
+		}
+	}
+	
+	/** Gets the JavaFX font posture for a given font. */
+	private javafx.scene.text.FontPosture getJavaFXFontPosture(Font font)
+	{
+		if (font.isItalic()) {
+			return javafx.scene.text.FontPosture.ITALIC;
+		} else {
+			return javafx.scene.text.FontPosture.REGULAR;
+		}
+	}
+	
+	/** Wrapper for JavaFX fonts as they do not retain knowledge of their bold/italics status. */
+	private static class JavaFXFont {
+		
+		private javafx.scene.text.Font _font = null;
+		
+		private boolean _isBold;
+		
+		private boolean _isItalics;
+		
+		/** Default constructor for internal use only. */
+		private JavaFXFont() {}
+		
+		public javafx.scene.text.Font getRealFont()
+		{
+			return _font;
+		}
+		
+		public String getFamily()
+		{
+			return _font.getFamily();
+		}
+		
+		public boolean isBold()
+		{
+			return _isBold;
+		}
+		
+		public boolean isItalics()
+		{
+			return _isItalics;
+		}
+		
+		public double getSize()
+		{
+			return _font.getSize();
+		}
+		
+		public static JavaFXFont getDefault()
+		{
+			JavaFXFont font = new JavaFXFont();
+			font._font = javafx.scene.text.Font.getDefault();
+			if (font._font == null) return null;
+			font._isBold = false;
+			font._isItalics = false;
+			return font;
+		}
+		
+		public static JavaFXFont loadFont(InputStream in, double size)
+		{
+			JavaFXFont font = new JavaFXFont();
+			font._font = javafx.scene.text.Font.loadFont(in, size);
+			if (font._font == null) return null;
+			font._isBold = false;
+			font._isItalics = false;
+			return font;
+		}
+		
+		public static JavaFXFont font(String family, FontWeight weight, FontPosture posture, double size)
+		{
+			JavaFXFont font = new JavaFXFont();
+			font._font = javafx.scene.text.Font.font(family, weight, posture, size);
+			if (font._font == null) return null;
+			font._isBold = (weight == FontWeight.BOLD);
+			font._isItalics = (posture == FontPosture.ITALIC);
+			return font;
+		}
+	}
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXGraphicsManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXGraphicsManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXGraphicsManager.java	(revision 1097)
@@ -0,0 +1,678 @@
+package org.expeditee.gio.javafx;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Stack;
+
+import org.expeditee.Util;
+import org.expeditee.core.Clip;
+import org.expeditee.core.Colour;
+import org.expeditee.core.Cursor;
+import org.expeditee.core.Dimension;
+import org.expeditee.core.Fill;
+import org.expeditee.core.Font;
+import org.expeditee.core.GradientFill;
+import org.expeditee.core.Image;
+import org.expeditee.core.Point;
+import org.expeditee.core.Stroke;
+import org.expeditee.core.TextLayout;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.core.bounds.PolygonBounds;
+import org.expeditee.gio.GraphicsManager;
+import org.expeditee.gio.GraphicsSurfaceStack;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.geometry.Bounds;
+import javafx.scene.Group;
+import javafx.scene.Scene;
+import javafx.scene.SnapshotParameters;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.effect.BlendMode;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.paint.Color;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+
+public class JavaFXGraphicsManager extends GraphicsManager {
+	
+	/** Singleton instance. */
+	private static JavaFXGraphicsManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXGraphicsManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXGraphicsManager();
+		
+		return _instance;
+	}
+
+	/** The largest size a window icon can be. */
+	private static final int MAX_ICON_SIZE = 256;
+	/** The smallest size a window icon can be. */
+	private static final int MIN_ICON_SIZE = 8;
+	
+	/** The startup thread for the JavaFX application. */
+	private static Thread _jfxAppThread;
+	
+	/** The stack of drawing surfaces. */
+	private GraphicsSurfaceStack<Canvas> _surfaceStack;
+	
+	/** A stack for surface clips to aid in clip clearing. */
+	private Clip _currentClip;
+	private Stack<Clip> _clipStack;
+	
+	private JavaFXGraphicsManager()
+	{
+		// Create the clip stack
+		_currentClip = null;
+		_clipStack = new Stack<Clip>();
+		
+		// Set the ExpediteeApplication manager
+		ExpediteeApplication.manager = this;
+		
+		// Start up the JavaFX thread on another thread as it blocks until program exit
+		_jfxAppThread = new Thread("_jfxAppThread") {
+			@Override
+			public void run()
+			{
+				Application.launch(ExpediteeApplication.class);
+			}
+		};
+		
+		// Block the main thread until the JavaFX thread is ready
+		synchronized(_jfxAppThread) {
+			_jfxAppThread.start();
+			try {
+				_jfxAppThread.wait();
+			} catch (InterruptedException e) {
+				System.err.println("Error while initialising GraphicsManager. Aborting...");
+				e.printStackTrace(System.err);
+				System.exit(1);
+			}
+		}
+		
+		// Create the surface stack
+		_surfaceStack = new GraphicsSurfaceStack<Canvas>() {
+			@Override
+			public Canvas getSurfaceFromImage(Image image)
+			{
+				Canvas canvas = new Canvas(image.getWidth(), image.getHeight());
+				canvas.getGraphicsContext2D().drawImage(JavaFXMiscManager.getIfUsingJavaFXImageManager().getInternalImage(image), 0, 0);
+				return canvas;
+			}
+			
+			@Override
+			public void setSurfaceClip(Canvas surface, Clip clip)
+			{
+				if (surface == null) return;
+				
+				if (clip != null && clip.isFullyClipped()) return;
+				
+				GraphicsContext g = surface.getGraphicsContext2D();
+				
+				// Restore the pre-clipped state
+				g.restore();
+				
+				// If we are clearing the clip, we are done
+				if (clip == null || clip.getBounds() == null) return;
+				
+				// Re-save the pre-clipped state
+				g.save();
+				
+				// Set the clip
+				g.beginPath();
+				AxisAlignedBoxBounds clipBounds = clip.getBounds();
+				g.rect(clipBounds.getMinX(), clipBounds.getMinY(), clipBounds.getWidth(), clipBounds.getHeight());
+				g.closePath();
+				g.clip();
+			}
+		};
+		_surfaceStack.setRootSurface(ExpediteeApplication.theCanvas);
+		
+		// Disable JavaFX's fullscreen exiting system
+		ExpediteeApplication.theStage.setFullScreenExitHint("");
+		ExpediteeApplication.theStage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH);
+		
+		// Block until the window is ready
+		JavaFXMiscManager.waitUntilPropertyEquals(ExpediteeApplication.theStage.showingProperty(), true);
+		
+	}
+	
+	@Override
+	protected GraphicsSurfaceStack<?> getGraphicsSurfaceStack()
+	{
+		return _surfaceStack;
+	}
+
+	@Override
+	public Dimension getScreenSize()
+	{
+		return JavaFXConversions.fromJavaFXRectangle2D(Screen.getPrimary().getBounds()).getSize();
+	}
+
+	@Override
+	public Point getWindowLocation()
+	{
+		return translateCanvasToScreen(new Point(0, 0));
+	}
+
+	@Override
+	public void setWindowLocation(Point p)
+	{
+		if (p == null) return;
+		
+		Point currentCanvasPos = getWindowLocation();
+		Point currentStagePos = new Point((int) ExpediteeApplication.theStage.getX(), (int) ExpediteeApplication.theStage.getY());
+		
+		Point delta = Point.difference(currentStagePos, currentCanvasPos);
+		
+		Point newStagePos = p.clone().add(delta);
+		
+		ExpediteeApplication.theStage.setX(newStagePos.x);
+		ExpediteeApplication.theStage.setY(newStagePos.y);
+	}
+
+	@Override
+	public Dimension getWindowSize()
+	{
+		return new Dimension((int) ExpediteeApplication.theScene.getWidth(), (int) ExpediteeApplication.theScene.getHeight());
+	}
+
+	@Override
+	public void setWindowSize(Dimension d)
+	{
+		if (d == null) return;
+
+		Dimension decorationDimensions = getDecorationDimensions();
+		
+		ExpediteeApplication.theStage.setWidth(d.width + decorationDimensions.width);
+		ExpediteeApplication.theStage.setHeight(d.height + decorationDimensions.height);
+		ExpediteeApplication.theCanvas.setWidth(d.width);
+		ExpediteeApplication.theCanvas.setHeight(d.height);
+		
+	}
+
+	@Override
+	public void requestFocus()
+	{
+		ExpediteeApplication.theStage.requestFocus();
+	}
+
+	@Override
+	public void setWindowIcon(Image image)
+	{
+		// TODO: Work out why this doesn't work. cts16
+		JavaFXImageManager imageManager = JavaFXMiscManager.getIfUsingJavaFXImageManager();
+		
+		if (imageManager == null || !imageManager.isImageValid(image)) return;
+
+		ExpediteeApplication.theStage.getIcons().clear();
+
+		ExpediteeApplication.theStage.getIcons().add(imageManager.getInternalImage(image));
+		
+		/*
+		Set<Image> iconSet = createIconSet(image);
+		
+		if (iconSet != null) {
+			for (Image icon : iconSet) ExpediteeApplication.theStage.getIcons().add(imageManager.getInternalImage(icon));
+		}
+		*/
+	}
+	
+	@Override
+	public void setWindowTitle(String title)
+	{
+		if (title == null) return;
+		
+		ExpediteeApplication.theStage.setTitle(title);
+	}
+	
+	private Point _preFullscreenWindowLocation = null;
+	private Dimension _preFullscreenWindowSize = null;
+
+	@Override
+	public boolean canGoFullscreen()
+	{
+		// TODO: Check if this is always true. cts16
+		return true;
+	}
+
+	@Override
+	public boolean isFullscreen()
+	{
+		return ExpediteeApplication.theStage.isFullScreen();
+	}
+
+	@Override
+	public void goFullscreen()
+	{
+		_preFullscreenWindowLocation = getWindowLocation();
+		_preFullscreenWindowSize = getWindowSize();
+		ExpediteeApplication.theStage.setFullScreen(true);
+	}
+
+	@Override
+	public void exitFullscreen()
+	{
+		ExpediteeApplication.theStage.setFullScreen(false);
+		setWindowLocation(_preFullscreenWindowLocation);
+		setWindowSize(_preFullscreenWindowSize);
+	}
+
+	@Override
+	public void pushDrawingSurface(Image image)
+	{
+		_surfaceStack.push(image);
+		_clipStack.push(_currentClip);
+		_currentClip = null;
+	}
+
+	@Override
+	public Image popDrawingSurface()
+	{
+		// Get the current canvas
+		Canvas canvas = _surfaceStack.getCurrentSurface();
+		
+		// Get its corresponding image
+		Image image = _surfaceStack.pop();
+		
+		// Check we haven't hit the bottom of the stack
+		if (image == null) return null;
+		
+		// Write the canvas contents back to the image
+		SnapshotParameters params = new SnapshotParameters();
+		params.setFill(Color.TRANSPARENT);
+		canvas.snapshot(params, JavaFXMiscManager.getIfUsingJavaFXImageManager().getInternalImage(image));
+		
+		// Reload the next surface's clip
+		_currentClip = _clipStack.pop();
+		
+		return image;
+	}
+
+	@Override
+	public void setCompositeAlpha(float alpha)
+	{
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		// Make sure we are setting to the base of the stack
+		g.restore();
+		
+		// Set the blend alpha
+		g.setGlobalBlendMode(BlendMode.SRC_OVER);
+		g.setGlobalAlpha(alpha);
+		
+		// Re-clip
+		_surfaceStack.ensureClip();
+	}
+
+	@Override
+	public void setAntialiasing(boolean on)
+	{
+		// TODO: Implement. cts16
+	}
+
+	@Override
+	public void setFont(Font font)
+	{
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		g.restore();
+		
+		g.setFont(JavaFXMiscManager.getIfUsingJavaFXFontManager().getInternalFont(font));
+
+		// Re-clip
+		_surfaceStack.ensureClip();
+	}
+
+	@Override
+	public void drawImage(Image image, Point topLeft, Dimension size, double angle, Point cropTopLeft, Dimension cropSize)
+	{
+		// Can't draw nothing
+		if (image == null) return;
+		
+		// Can't draw nowhere
+		if (topLeft == null) return;
+		
+		// If the cropped area is degenerate, abort
+		if (cropSize != null && (cropSize.width <= 0 || cropSize.height <= 0)) return;
+		
+		// If the crop area is the entire image, pretend we are not cropping
+		if (Point.ORIGIN.equals(cropTopLeft) && image.getSize().equals(cropSize)) {
+			cropTopLeft = null;
+			cropSize = null;
+		}
+
+		JavaFXImageManager manager = JavaFXMiscManager.getIfUsingJavaFXImageManager();
+		
+		// Default is the entire image
+		Image croppedImage = image;
+		
+		// If we are cropping, do this now into a temporary image
+		if (cropTopLeft != null && cropSize != null) {
+			croppedImage = manager.createImage(cropSize.width, cropSize.height);
+			manager.getInternalImage(croppedImage).getPixelWriter().setPixels(0, 0, cropSize.width, cropSize.height, manager.getInternalImage(image).getPixelReader(), cropTopLeft.x, cropTopLeft.y);
+		}
+		
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		g.save();
+		if (size != null) g.scale(((double) size.width) / croppedImage.getWidth(), ((double) size.height) / croppedImage.getHeight());
+		if (angle != 0.0) g.rotate(angle);
+		if (topLeft != null) g.translate(topLeft.x, topLeft.y);
+		g.drawImage(JavaFXMiscManager.getIfUsingJavaFXImageManager().getInternalImage(croppedImage), 0, 0);
+		g.restore();
+	}
+
+	@Override
+	public void drawRectangle(Point topLeft, Dimension size, double angle, Fill fill, Colour borderColour, Stroke borderStroke, Dimension cornerRadius)
+	{
+		// Can't draw nowhere
+		if (topLeft == null) return;
+		
+		// Can't draw nothing
+		if (size == null) return;
+		
+		// If not filled and has no border, why bother?
+		if (fill == null && (borderColour == null || borderStroke == null)) return;
+		
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		g.save();
+
+		Point centre = new Point(topLeft.x + size.width / 2, topLeft.y + size.height / 2);
+		g.translate(-centre.x, -centre.y);
+		if (angle != 0.0) g.rotate(angle);
+		g.translate(centre.x, centre.y);
+		
+		if (fill != null) {
+			if (fill instanceof GradientFill) {
+				g.setFill(JavaFXConversions.toJavaFXLinearGradient((GradientFill) fill));
+			} else {
+				g.setFill(JavaFXConversions.toJavaFXColor(fill.getColour()));
+			}
+			
+			if (cornerRadius == null) {
+				g.fillRect(topLeft.x, topLeft.y, size.width, size.height);
+			} else {
+				g.fillRoundRect(topLeft.x, topLeft.y, size.width, size.height, cornerRadius.width, cornerRadius.height);
+			}
+		}
+		
+		if (borderColour != null && borderStroke != null) {
+			g.setStroke(JavaFXConversions.toJavaFXColor(borderColour));
+			setStroke(g, borderStroke);
+
+			if (cornerRadius == null) {
+				g.strokeRect(topLeft.x, topLeft.y, size.width, size.height);
+			} else {
+				g.strokeRoundRect(topLeft.x, topLeft.y, size.width, size.height, cornerRadius.width, cornerRadius.height);
+			}
+		}
+		
+		g.restore();
+	}
+
+	@Override
+	public void drawOval(Point centre, Dimension diameters, double angle, Fill fill, Colour borderColour, Stroke borderStroke)
+	{
+		// Can't draw nowhere
+		if (centre == null) return;
+		
+		// Can't draw nothing
+		if (diameters == null) return;
+		
+		// If not filled and has no border, why bother?
+		if (fill == null && (borderColour == null || borderStroke == null)) return;
+		
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		g.save();
+
+		if (angle != 0.0) {
+			g.translate(-centre.x, -centre.y);
+			g.rotate(angle);
+			g.translate(centre.x, centre.y);
+		}
+		
+		if (fill != null) {
+			if (fill instanceof GradientFill) {
+				g.setFill(JavaFXConversions.toJavaFXLinearGradient((GradientFill) fill));
+			} else {
+				g.setFill(JavaFXConversions.toJavaFXColor(fill.getColour()));
+			}
+			
+			g.fillOval(centre.x - diameters.width / 2, centre.y - diameters.height / 2, diameters.width, diameters.height);
+		}
+		
+		if (borderColour != null && borderStroke != null) {
+			g.setStroke(JavaFXConversions.toJavaFXColor(borderColour));
+			setStroke(g, borderStroke);
+
+			g.strokeOval(centre.x - diameters.width / 2, centre.y - diameters.height / 2, diameters.width, diameters.height);
+		}
+		
+		g.restore();
+	}
+
+	@Override
+	public void drawPolygon(PolygonBounds points, Point offset, Dimension scale, double angle, Fill fill, Colour borderColour, Stroke borderStroke)
+	{
+		if (points == null || points.getPointCount() < 2) return;
+		if (fill == null && (borderColour == null || borderStroke == null)) return;
+		
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		g.save();
+
+		Point centre = points.getCentre();
+		g.translate(-centre.x, -centre.y);
+		if (angle != 0.0) g.rotate(angle);
+		if (scale != null) {
+			double xScale = ((double) scale.width) / (points.getMaxX() - points.getMinX());
+			double yScale = ((double) scale.height) / (points.getMaxY() - points.getMinY());
+			g.scale(xScale, yScale);
+		}
+		g.translate(centre.x, centre.y);
+		if (offset != null) g.translate(offset.x, offset.y);
+		
+		Point[] pointArray = points.toArray();
+		int nPoints = pointArray.length;
+		double[] xArray = new double[nPoints];
+		double[] yArray = new double[nPoints];
+		
+		for (int i = 0; i < nPoints; i++) {
+			xArray[i] = pointArray[i].x + 0.5;
+			yArray[i] = pointArray[i].y + 0.5;
+		}
+		
+		if (fill != null) {
+			if (fill instanceof GradientFill) {
+				g.setFill(JavaFXConversions.toJavaFXLinearGradient((GradientFill) fill));
+			} else {
+				g.setFill(JavaFXConversions.toJavaFXColor(fill.getColour()));
+			}
+			
+			g.fillPolygon(xArray, yArray, nPoints);
+		}
+		
+		if (borderColour != null && borderStroke != null) {
+			g.setStroke(JavaFXConversions.toJavaFXColor(borderColour));
+			setStroke(g, borderStroke);
+
+			if (points.isClosed()) {
+				g.strokePolygon(xArray, yArray, nPoints);
+			} else {
+				g.strokePolyline(xArray, yArray, nPoints);
+			}
+		}
+		
+		g.restore();
+	}
+
+	@Override
+	public void drawLine(int x1, int y1, int x2, int y2, Colour colour, Stroke stroke)
+	{
+		if (colour == null || stroke == null) return;
+		
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		g.save();
+		
+		g.setStroke(JavaFXConversions.toJavaFXColor(colour));
+		setStroke(g, stroke);
+		
+		g.beginPath();
+		g.moveTo(x1 + 0.5, y1 + 0.5);
+		g.lineTo(x2 + 0.5, y2 + 0.5);
+		g.stroke();
+		
+		g.restore();
+	}
+
+	@Override
+	public void drawString(String string, Point position, Font font, Colour colour)
+	{
+		// TODO: Implement. cts16
+	}
+	
+	@Override
+	public void drawTextLayout(TextLayout layout, Point position, Colour colour)
+	{
+		if (layout == null || position == null || colour == null) return;
+		
+		JavaFXTextLayoutManager layoutManager = JavaFXMiscManager.getIfUsingJavaFXTextLayoutManager();
+		if (layoutManager == null) {
+			drawString(layout.getLine(), position, layout.getFont(), colour);
+			return;
+		}
+		
+		javafx.scene.text.Text text = layoutManager.getInternalLayout(layout);
+
+		GraphicsContext g = _surfaceStack.getCurrentSurface().getGraphicsContext2D();
+		
+		g.save();
+		
+		g.setFont(text.getFont());
+		g.setTextAlign(text.getTextAlignment());
+		
+		g.setFill(JavaFXConversions.toJavaFXColor(colour));
+		
+		g.fillText(text.getText(), position.x, position.y);
+		
+		g.restore();
+	}
+
+	@Override
+	public void setCursor(Cursor cursor)
+	{
+		// TODO: Implement. cts16
+	}
+
+	@Override
+	public Dimension getBestCursorSize(Dimension desiredSize)
+	{
+		// TODO: Implement. cts16
+		return new Dimension(32, 32);
+	}
+
+	@Override
+	public boolean showDialog(String title, String message)
+	{
+		// TODO: Implement. cts16
+		return false;
+	}
+
+	@Override
+	public Dimension getCurrentDrawingSurfaceSize()
+	{
+		Canvas currentSurface = _surfaceStack.getCurrentSurface();
+		
+		int width = (int) currentSurface.getWidth();
+		int height = (int) currentSurface.getHeight();
+		
+		return new Dimension(width, height);
+	}
+	
+	private Point translateCanvasToScreen(Point p)
+	{
+		if (p == null) return null;
+		
+		Bounds canvasBounds = ExpediteeApplication.theCanvas.localToScreen(ExpediteeApplication.theCanvas.getBoundsInLocal());
+		
+		return p.clone().add(canvasBounds.getMinX(), canvasBounds.getMinY());
+	}
+	
+	private Dimension getDecorationDimensions()
+	{
+		int decorWidth = (int) (ExpediteeApplication.theStage.getWidth() - ExpediteeApplication.theScene.getWidth());
+		int decorHeight = (int) (ExpediteeApplication.theStage.getHeight() - ExpediteeApplication.theScene.getHeight());
+		
+		return new Dimension(decorWidth, decorHeight);
+	}
+
+	private void setStroke(GraphicsContext g, Stroke stroke)
+	{
+		g.setLineWidth(stroke.thickness);
+		g.setLineCap(JavaFXConversions.toJavaFXStrokeLineCap(stroke.capType));
+		g.setLineJoin(JavaFXConversions.toJavaFXStrokeLineJoin(stroke.joinType));
+		g.setLineDashes(Util.toDoubleArray(stroke.dashArray));
+		g.setLineDashOffset(stroke.dashPhase);
+	}
+	
+	private Set<Image> createIconSet(Image image)
+	{
+		if (image == null) return null;
+		
+		Set<Image> iconSet = new HashSet<Image>();
+		
+		for (int size = MAX_ICON_SIZE; size >= MIN_ICON_SIZE; size >>= 1) {
+			Image tempImage = Image.createImage(size, size);
+			this.pushDrawingSurface(tempImage);
+			this.drawImage(image, Point.ORIGIN, new Dimension(size, size));
+			this.popDrawingSurface();
+			iconSet.add(tempImage);
+		}
+		
+		return iconSet;
+	}
+	
+	public static class ExpediteeApplication extends Application {
+		
+		public static Stage theStage = null;
+		public static Scene theScene = null;
+		public static Group theGroup = null;
+		public static Canvas theCanvas = null;
+		
+		public static JavaFXGraphicsManager manager = null;
+
+		@Override
+		public void start(Stage primaryStage) throws Exception
+		{
+			// Set up the canvas for drawing operations
+			theStage = primaryStage;
+			theGroup = new Group();
+			theScene = new Scene(theGroup);
+			theCanvas = new Canvas();
+			theGroup.getChildren().add(theCanvas);
+			
+			// Set the scene
+			manager.setWindowIcon();
+			primaryStage.setScene(theScene);
+			primaryStage.show();
+			
+			Platform.runLater(new Runnable() {
+				@Override
+				public void run() {
+					synchronized (_jfxAppThread) {
+						_jfxAppThread.notify();
+					}
+				}
+			});
+		}
+		
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXImageManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXImageManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXImageManager.java	(revision 1097)
@@ -0,0 +1,309 @@
+package org.expeditee.gio.javafx;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+
+import javax.imageio.ImageIO;
+
+import org.expeditee.core.Colour;
+import org.expeditee.core.Image;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.gio.ImageManager;
+
+import com.sun.pdfview.PDFPage;
+
+import javafx.embed.swing.SwingFXUtils;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.WritableImage;
+
+public class JavaFXImageManager extends ImageManager {
+	
+	/** Singleton instance. */
+	private static JavaFXImageManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXImageManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXImageManager();
+		
+		return _instance;
+	}
+
+	/** Mapping from image handles to actual internal images. */
+	private HashMap<Long, javafx.scene.image.WritableImage> _imageMap;
+	
+	/** Constructor. */
+	private JavaFXImageManager()
+	{
+		// Initialise the map
+		_imageMap = new HashMap<Long, javafx.scene.image.WritableImage>();
+	}
+
+	@Override
+	public Image createImage(int width, int height, boolean hintUseAcceleratedMemory)
+	{
+		// Note: hintUseAcceleratedMemory is ignored. cts16
+		
+		// Ensure width and height are sane
+		if (width <= 0 || height <= 0) return null;
+		
+		// Create the image
+		WritableImage jfxImage = new WritableImage(width, height);
+
+		// Register and return the handle
+		return register(jfxImage, false);
+	}
+
+	@Override
+	public Image createImage(int width, int height, int[] pixelData)
+	{
+		// Ensure width and height are sane
+		if (width <= 0 || height <= 0) return null;
+		
+		// Make sure the caller has provided enough pixel data
+		if (pixelData == null || pixelData.length < (width * height)) return null;
+		
+		// Create the image
+		WritableImage jfxImage = new WritableImage(width, height);
+		
+		// Copy the pixel data to the image
+		jfxImage.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), pixelData, 0, width);
+
+		// Register and return the handle
+		return register(jfxImage, false);
+	}
+
+	@Override
+	public Image createImage(int width, int height, PDFPage page)
+	{
+		// TODO: Currently isn't implemented properly. cts16
+		return createImage(width, height);
+	}
+	
+	/** Create a registration for an internal-type image. Not part of the general public interface. */
+	public Image createImage(javafx.scene.image.Image image)
+	{
+		// Make sure we have a valid image
+		if (image == null || image.getWidth() == 0.0 || image.getHeight() == 0.0) return null;
+		
+		// Create a writable copy of the original
+		javafx.scene.image.WritableImage writableImage = new WritableImage(image.getPixelReader(), (int) image.getWidth(), (int) image.getHeight());
+		
+		// Register the image
+		return register(writableImage, false);
+	}
+
+	@Override
+	public Image createImageAsCroppedCopy(Image orig, int x, int y, int width, int height)
+	{
+		// Ensure width and height are sane
+		if (width <= 0 || height <= 0) return null;
+		
+		// Make sure the original is a valid image
+		javafx.scene.image.WritableImage jfxOrig = getInternalImage(orig);
+		if (jfxOrig == null) return null;
+		
+		// Make sure the given coordinate are within the bounds of the image
+		AxisAlignedBoxBounds selectedArea = new AxisAlignedBoxBounds(x, y, width, height);
+		if (!orig.getBounds().completelyContains(selectedArea)) return null;
+		
+		// Create a new image that is the cropped size
+		javafx.scene.image.WritableImage jfxCropped = new WritableImage(jfxOrig.getPixelReader(), x, y, width, height);
+		
+		// Register and return the handle
+		return register(jfxCropped, false);
+	}
+
+	@Override
+	public Image getImage(URL url)
+	{
+		// JavaFX loads all images via a string url
+		return getImage(url.toString());
+	}
+
+	@Override
+	public Image getImage(String filename)
+	{
+		// Make sure we have a filename
+		if (filename == null) return null;
+		
+		// Make sure we include the file protocol for files
+		if (!filename.startsWith("file")) {
+			try {
+				URL url = new URL("file", null, filename);
+				filename = url.toString();
+			} catch (MalformedURLException e) {
+				e.printStackTrace();
+				return null;
+			}
+		}
+		// Load the file as read only
+		javafx.scene.image.Image jfxReadOnlyImage;
+		try {
+			jfxReadOnlyImage = new javafx.scene.image.Image(filename, false);
+		} catch (Exception e) {
+			return null;
+		}
+		
+		// Create the writable copy
+		javafx.scene.image.WritableImage jfxImage = new javafx.scene.image.WritableImage(jfxReadOnlyImage.getPixelReader(), (int) jfxReadOnlyImage.getWidth(), (int) jfxReadOnlyImage.getHeight());
+
+		// Register and return the handle
+		return register(jfxImage, false);
+	}
+
+	@Override
+	public Image getImage(HttpURLConnection connection) throws IOException
+	{
+		// Make sure the connection is valid
+		if (connection == null) return null;
+		
+		// Load the image as read only
+		javafx.scene.image.Image jfxReadOnlyImage = new javafx.scene.image.Image(connection.getInputStream());
+		
+		// Block until jfxReadOnlyImage has a valid width/height
+		ensureLoaded(jfxReadOnlyImage);
+		
+		// Create the writable copy
+		javafx.scene.image.WritableImage jfxImage = new javafx.scene.image.WritableImage(jfxReadOnlyImage.getPixelReader(), (int) jfxReadOnlyImage.getWidth(), (int) jfxReadOnlyImage.getHeight());
+
+		// Register and return the handle
+		return register(jfxImage, false);
+	}
+
+	@Override
+	public synchronized void releaseImage(org.expeditee.core.Image image)
+	{
+		// Make sure the handle corresponds to a valid JavaFX image
+		javafx.scene.image.WritableImage jfxImage = getInternalImage(image);
+		if (jfxImage == null) return;
+		
+		// Release and forget about the JavaFX image
+		if (jfxImage != null) {
+			_imageMap.remove(image.getHandle());
+		}
+	}
+
+	@Override
+	public synchronized boolean isImageValid(Image image)
+	{
+		// Null image is invalid
+		if (image == null) return false;
+		
+		// Image is valid if it's in the map
+		return _imageMap.containsKey(image.getHandle());
+	}
+
+	@Override
+	public int getWidth(Image image)
+	{
+		// Make sure the handle corresponds to a valid JavaFX image
+		javafx.scene.image.WritableImage jfxImage = getInternalImage(image);
+		if (jfxImage == null) return Image.INVALID_SIZE;
+		
+		// Return the width
+		return (int) jfxImage.getWidth();
+	}
+
+	@Override
+	public int getHeight(Image image)
+	{
+		// Make sure the handle corresponds to a valid JavaFX image
+		javafx.scene.image.WritableImage jfxImage = getInternalImage(image);
+		if (jfxImage == null) return Image.INVALID_SIZE;
+		
+		// Return the height
+		return (int) jfxImage.getHeight();
+	}
+
+	@Override
+	public Colour[] getPixels(Image image, int x, int y, int width, int height)
+	{
+		// Make sure we have a valid image
+		javafx.scene.image.WritableImage jfxImage = getInternalImage(image);
+		if (jfxImage == null) return null;
+		
+		// Make sure width and height are reasonable
+		if (width <= 0 || height <= 0) return null;
+		
+		// Make sure the given coordinate are within the bounds of the image
+		AxisAlignedBoxBounds selectedArea = new AxisAlignedBoxBounds(x, y, width, height);
+		if (!image.getBounds().completelyContains(selectedArea)) return null;
+		
+		// Create an array to get the pixel values
+		int[] pixels = new int[width * height];
+		
+		// Read the values of the desired pixels
+		jfxImage.getPixelReader().getPixels(x, y, width, height, PixelFormat.getIntArgbInstance(), pixels, 0, width);
+		
+		// Return the colour of the grabbed pixel
+		Colour[] ret = new Colour[width * height];
+		for (int i = 0; i < (width * height); i++) {
+			ret[i] = Colour.fromARGB32BitPacked(pixels[i]);
+		}
+		return ret;
+	}
+
+	@Override
+	public void setPixel(Image image, int x, int y, Colour c)
+	{
+		// Make sure we have a valid image
+		javafx.scene.image.WritableImage jfxImage = getInternalImage(image);
+		if (jfxImage == null) return;
+		
+		// Make sure we are inside the image
+		if (!image.getBounds().contains(x, y)) return;
+		
+		// Set the colour of the pixel
+		jfxImage.getPixelWriter().setColor(x, y, JavaFXConversions.toJavaFXColor(c));
+	}
+
+	@Override
+	public boolean writeImageToDisk(Image image, String format, File file) throws IOException
+	{
+		// Validate parameters
+		if (format == null || file == null) return false;
+		
+		// Make sure we have a valid image
+		javafx.scene.image.Image jfxImage = getInternalImage(image);
+		if (jfxImage == null) return false;
+		
+		// Create a Swing image (the internets said this is how you do it :P)
+		// TODO: see if there is a Swing-less way of doing this. cts16
+		java.awt.image.BufferedImage swingImage = SwingFXUtils.fromFXImage(jfxImage, null);
+		
+		// Write the image to disk
+		return ImageIO.write(swingImage, format, file);
+	}
+	
+	/** Creates and returns a handle for the given JavaFX image to use for future reference. */ 
+	private synchronized Image register(javafx.scene.image.WritableImage jfxImage, boolean fromDisk)
+	{
+		// TODO: Add animators. cts16
+		// TODO: Only add animators for images we know are animated. cts16
+		
+		Image image = Image.get(fromDisk);
+		
+		_imageMap.put(image.getHandle(), jfxImage);
+		
+		return image;
+	}
+	
+	/** Gets the JavaFX image associated with the given handle, or null if none is. */
+	public synchronized javafx.scene.image.WritableImage getInternalImage(Image image)
+	{
+		if (!isImageValid(image)) return null;
+		
+		return _imageMap.get(image.getHandle());
+	}
+	
+	/** Blocks the calling thread until the given image is fully loaded. */
+	private void ensureLoaded(javafx.scene.image.Image jfxImage)
+	{
+		JavaFXMiscManager.waitUntilPropertyEquals(jfxImage.progressProperty(), 1.0);
+	}
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXInputManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXInputManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXInputManager.java	(revision 1097)
@@ -0,0 +1,282 @@
+package org.expeditee.gio.javafx;
+
+import java.awt.AWTException;
+import java.awt.Robot;
+
+import org.expeditee.core.Dimension;
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.InputManager;
+import org.expeditee.gio.input.InputEvent;
+import org.expeditee.gio.input.KBMInputEvent;
+import org.expeditee.gio.input.InputEvent.InputType;
+import org.expeditee.gio.javafx.JavaFXGraphicsManager.ExpediteeApplication;
+import org.expeditee.items.Text;
+
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.stage.WindowEvent;
+import javafx.util.Duration;
+
+public class JavaFXInputManager extends InputManager {
+	
+	/** Singleton instance. */
+	private static JavaFXInputManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXInputManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXInputManager();
+		
+		return _instance;
+	}
+	
+	// TODO: Is there a JavaFX way of controlling the mouse? cts16
+	/** For robotically controlling the mouse position. */
+	private Robot _robot;
+	
+	/** JavaFX sends spurious enter/exit events when pressing the middle mouse button. Ignore these. */
+	boolean _lastExitSpurious = false;
+
+	private JavaFXInputManager()
+	{
+		JavaFXInputManager manager = this;
+		
+		// Exit the program on window closed
+		JavaFXGraphicsManager.ExpediteeApplication.theStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
+			@Override
+			public void handle(WindowEvent event)
+			{
+				distributeWindowEvent(WindowEventType.WINDOW_CLOSED);
+			}
+		});
+		
+		// Register to receive window resize events
+		ChangeListener<Number> windowSizeListener = new ChangeListener<Number>() {
+			@Override
+			public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
+			{
+				Dimension windowSize = EcosystemManager.getGraphicsManager().getWindowSize();
+				ExpediteeApplication.theCanvas.setWidth(windowSize.width);
+				ExpediteeApplication.theCanvas.setHeight(windowSize.height);
+				distributeWindowEvent(WindowEventType.WINDOW_RESIZED);
+			}
+		};
+		JavaFXGraphicsManager.ExpediteeApplication.theStage.widthProperty().addListener(windowSizeListener);
+		JavaFXGraphicsManager.ExpediteeApplication.theStage.heightProperty().addListener(windowSizeListener);
+		
+		// Handle mouse wheel events
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(ScrollEvent.SCROLL, new EventHandler<ScrollEvent>() {
+			@Override
+			public void handle(ScrollEvent event) {
+				try {
+					// Create an input event
+					Integer rotation = new Integer((int) (event.getDeltaY() / event.getMultiplierY()));
+					InputEvent expEvent = new KBMInputEvent(KBMInputEvent.EventType.MOUSE_WHEEL_SCROLL, rotation);
+					
+					// Translate and perform the input
+					distributeInputEvent(expEvent);
+				} catch (Exception exception) {
+					System.err.println(exception.getMessage());
+				}
+			}
+		});
+		
+		// Handle mouse movement events
+		EventHandler<MouseEvent> mouseMovementEventHandler = new EventHandler<MouseEvent>() {
+			@Override
+			public void handle(MouseEvent event) {
+				try {
+					// Create an input event
+					Point movedFrom = getCursorPosition();
+					Point movedTo = new Point((int) event.getSceneX(), (int) event.getSceneY());
+					InputEvent expEvent = new KBMInputEvent(KBMInputEvent.EventType.MOUSE_MOVE, new Line(movedFrom, movedTo));
+					
+					// Translate and perform the input
+					distributeInputEvent(expEvent);
+				} catch (Exception exception) {
+					System.err.println(exception.getMessage());
+				}
+				
+				// Update the cursor position
+				updateCursorPosition((int) event.getSceneX(), (int) event.getSceneY());
+			}
+		};
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(MouseEvent.MOUSE_DRAGGED, mouseMovementEventHandler);
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(MouseEvent.MOUSE_MOVED, mouseMovementEventHandler);
+		
+		// Handle mouse press/release events
+		EventHandler<MouseEvent> mousePressReleaseEventHandler = new EventHandler<MouseEvent>() {
+			@Override
+			public void handle(MouseEvent event) {
+				// Work out which button was pressed
+				KBMInputEvent.MouseButton button;
+				switch (event.getButton()) {
+					case PRIMARY:
+						button = KBMInputEvent.MouseButton.LEFT;
+						break;
+					case MIDDLE:
+						button = KBMInputEvent.MouseButton.MIDDLE;
+						break;
+					case SECONDARY:
+						button = KBMInputEvent.MouseButton.RIGHT;
+						break;
+					default:
+						return;
+				}
+				
+				try {
+					// Create an input event
+					KBMInputEvent.EventType type = (event.getEventType() == MouseEvent.MOUSE_PRESSED) ? KBMInputEvent.EventType.MOUSE_BUTTON_DOWN : KBMInputEvent.EventType.MOUSE_BUTTON_UP;
+					InputEvent expEvent = new KBMInputEvent(type, button);
+					
+					// Translate and perform the input
+					distributeInputEvent(expEvent);
+				} catch (Exception exception) {
+					System.err.println(exception.getMessage());
+				}
+			}
+		};
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(MouseEvent.MOUSE_PRESSED, mousePressReleaseEventHandler);
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(MouseEvent.MOUSE_RELEASED, mousePressReleaseEventHandler);
+
+		
+		// Handle mouse-enters-window events
+		JavaFXGraphicsManager.ExpediteeApplication.theCanvas.addEventHandler(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>() {
+			@Override
+			public void handle(MouseEvent event) {
+				if (!_lastExitSpurious) distributeWindowEvent(WindowEventType.MOUSE_ENTERED_WINDOW);
+			}
+		});
+		
+		// Handle mouse-exits-window events
+		JavaFXGraphicsManager.ExpediteeApplication.theCanvas.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
+			@Override
+			public void handle(MouseEvent event) {
+				_lastExitSpurious = event.isMiddleButtonDown();
+				if (!_lastExitSpurious) distributeWindowEvent(WindowEventType.MOUSE_EXITED_WINDOW);
+			}
+		});
+		
+		// Handle key-typed events
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(KeyEvent.KEY_TYPED, new EventHandler<KeyEvent>() {
+			@Override
+			public void handle(KeyEvent event) {
+				// Ignore control characters
+				if (event.isControlDown() || event.isAltDown()) return;
+				
+				String typedText = event.getCharacter();
+				
+				for (int i = 0; i < typedText.length(); i++) {
+					try {
+						// Ignore Esc typing
+						if (typedText.charAt(i) == Text.ESC_CHARACTER) continue;
+						
+						// Create an input event
+						Character character = new Character(typedText.charAt(i));
+						InputEvent expEvent = new KBMInputEvent(KBMInputEvent.EventType.CHAR_TYPED, character);
+						
+						// Translate and perform the input
+						distributeInputEvent(expEvent);
+					} catch (Exception exception) {
+						System.err.println(exception.getMessage());
+					}
+				}
+			}
+		});
+		
+		// Handle key pressed/released events
+		EventHandler<KeyEvent> keyPressReleaseEventHandler = new EventHandler<KeyEvent>() {
+			@Override
+			public void handle(KeyEvent event) {
+
+				KBMInputEvent.Key key = JavaFXConversions.fromJavaFXKeyCode(event.getCode());
+				if (key == null) return;
+				
+				try {
+					// Create an input event
+					KBMInputEvent.EventType type = (event.getEventType() == KeyEvent.KEY_PRESSED) ? KBMInputEvent.EventType.KEY_DOWN : KBMInputEvent.EventType.KEY_UP;
+					InputEvent expEvent = new KBMInputEvent(type, key);
+					
+					// Translate and perform the input
+					distributeInputEvent(expEvent);
+				} catch (Exception exception) {
+					System.err.println(exception.getMessage());
+				}
+			}
+		};
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(KeyEvent.KEY_PRESSED, keyPressReleaseEventHandler);
+		JavaFXGraphicsManager.ExpediteeApplication.theScene.addEventHandler(KeyEvent.KEY_RELEASED, keyPressReleaseEventHandler);
+		
+		// Set up the robot (for controlling the mouse)
+		// TODO: What to do if robot throws exception??? cts16
+		try {
+			_robot = new Robot();
+		} catch (AWTException e) {
+			e.printStackTrace();
+		}
+		
+		_timer = new Timeline();
+		_timer.cycleCountProperty().set(1);
+	}
+
+	@Override
+	protected Point getRealCursorPosition()
+	{
+		// Can't get the real mouse position. Not a big deal as only used for initialisation
+		// and is forgotten as soon as the mouse moves.
+		return new Point(0, 0);
+	}
+	
+	@Override
+	public void setCursorPosition(Point position)
+	{
+		Point screenPosition = windowToScreenPosition(position);
+		_robot.mouseMove(screenPosition.x, screenPosition.y);
+	}
+
+	@Override
+	protected boolean isInputTypeSupported(InputType type)
+	{
+		// Only keyboard/mouse input is currently supported
+		if (type == InputEvent.InputType.KBM) return true;
+		
+		return false;
+	}
+	
+	/** Converts a location given in window-space to screen-space. */
+	private Point windowToScreenPosition(Point windowPosition)
+	{
+		Point windowTopLeft = EcosystemManager.getGraphicsManager().getWindowLocation();
+		
+		return windowPosition.clone().add(windowTopLeft);
+	}
+
+	private Timeline _timer;
+	
+	@Override
+	protected void updateTimer(long nextTimeout)
+	{
+		JavaFXInputManager jfxInputManager = this;
+		
+		if (nextTimeout < 0) nextTimeout = 0;
+		_timer.stop();
+		_timer.getKeyFrames().clear();
+		_timer.getKeyFrames().add(new KeyFrame(new Duration(nextTimeout), new EventHandler<ActionEvent>() {
+			@Override
+			public void handle(ActionEvent event) {
+				jfxInputManager.triggerTimeoutEvents();
+			}
+		}));
+		_timer.playFromStart();
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXMiscManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXMiscManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXMiscManager.java	(revision 1097)
@@ -0,0 +1,183 @@
+package org.expeditee.gio.javafx;
+
+import org.expeditee.gio.ClipboardManager;
+import org.expeditee.gio.DragAndDropManager;
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.FontManager;
+import org.expeditee.gio.GraphicsManager;
+import org.expeditee.gio.ImageManager;
+import org.expeditee.gio.InputManager;
+import org.expeditee.gio.MiscManager;
+import org.expeditee.gio.TextLayoutManager;
+
+import javafx.application.Platform;
+import javafx.beans.property.ReadOnlyProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+
+public class JavaFXMiscManager extends MiscManager {
+	
+	/** Singleton instance. */
+	private static JavaFXMiscManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXMiscManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXMiscManager();
+		
+		return _instance;
+	}
+
+	/** Gets the JavaFXClipboardManager if it being used. */
+	public static JavaFXClipboardManager getIfUsingJavaFXClipboardManager()
+	{
+		ClipboardManager manager = EcosystemManager.getClipboardManager();
+		if (manager instanceof JavaFXClipboardManager) {
+			return (JavaFXClipboardManager) manager;
+		}
+		
+		return null;
+	}
+
+	/** Gets the JavaFXDragAndDropManager if it being used. */
+	public static JavaFXDragAndDropManager getIfUsingJavaFXDragAndDropManager()
+	{
+		DragAndDropManager manager = EcosystemManager.getDragAndDropManager();
+		if (manager instanceof JavaFXDragAndDropManager) {
+			return (JavaFXDragAndDropManager) manager;
+		}
+		
+		return null;
+	}
+
+	/** Gets the JavaFXFontManager if it being used. */
+	public static JavaFXFontManager getIfUsingJavaFXFontManager()
+	{
+		FontManager manager = EcosystemManager.getFontManager();
+		if (manager instanceof JavaFXFontManager) {
+			return (JavaFXFontManager) manager;
+		}
+		
+		return null;
+	}
+
+	/** Gets the JavaFXGraphicsManager if it being used. */
+	public static JavaFXGraphicsManager getIfUsingJavaFXGraphicsManager()
+	{
+		GraphicsManager manager = EcosystemManager.getGraphicsManager();
+		if (manager instanceof JavaFXGraphicsManager) {
+			return (JavaFXGraphicsManager) manager;
+		}
+		
+		return null;
+	}
+
+	/** Gets the JavaFXImageManager if it being used. */
+	public static JavaFXImageManager getIfUsingJavaFXImageManager()
+	{
+		ImageManager manager = EcosystemManager.getImageManager();
+		if (manager instanceof JavaFXImageManager) {
+			return (JavaFXImageManager) manager;
+		}
+		
+		return null;
+	}
+
+	/** Gets the JavaFXInputManager if it being used. */
+	public static JavaFXInputManager getIfUsingJavaFXInputManager()
+	{
+		InputManager manager = EcosystemManager.getInputManager();
+		if (manager instanceof JavaFXInputManager) {
+			return (JavaFXInputManager) manager;
+		}
+		
+		return null;
+	}
+
+	/** Gets the JavaFXMiscManager if it being used. */
+	public static JavaFXMiscManager getIfUsingJavaFXMiscManager()
+	{
+		MiscManager manager = EcosystemManager.getMiscManager();
+		if (manager instanceof JavaFXMiscManager) {
+			return (JavaFXMiscManager) manager;
+		}
+		
+		return null;
+	}
+
+	/** Gets the JavaFXTextLayoutManager if it being used. */
+	public static JavaFXTextLayoutManager getIfUsingJavaFXTextLayoutManager()
+	{
+		TextLayoutManager manager = EcosystemManager.getTextLayoutManager();
+		if (manager instanceof JavaFXTextLayoutManager) {
+			return (JavaFXTextLayoutManager) manager;
+		}
+		
+		return null;
+	}
+	
+	/** Blocks until the value of the given property equals the given value. */
+	public static <T> void waitUntilPropertyEquals(ReadOnlyProperty<? extends T> property, T value)
+	{
+		if (property.getValue().equals(value)) return;
+		
+		ChangeListener<T> listener = new ChangeListener<T>() {
+			@Override
+			public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
+				if (newValue == value || newValue.equals(value)) {
+					synchronized (this) {
+						notify();
+					}
+				}
+			}
+		};
+		
+		synchronized (listener) {
+			property.addListener(listener);
+			while (!property.getValue().equals(value)) {
+				try {
+					listener.wait();
+				} catch (InterruptedException e) {
+				}
+			}
+			property.removeListener(listener);
+		}
+	}
+	
+	@Override
+	public void beep() {
+		// TODO Auto-generated method stub
+		
+	}
+
+	@Override
+	public boolean browse(String url) {
+		// TODO Auto-generated method stub
+		return false;
+	}
+
+	@Override
+	public String print(String file) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	@Override
+	public String open(String file) {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	@Override
+	protected void scheduleRunnable(Runnable routine)
+	{
+		Platform.runLater(routine);
+	}
+
+	@Override
+	protected boolean isGIOThread()
+	{
+		return Platform.isFxApplicationThread();
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/javafx/JavaFXTextLayoutManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/javafx/JavaFXTextLayoutManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/javafx/JavaFXTextLayoutManager.java	(revision 1097)
@@ -0,0 +1,313 @@
+package org.expeditee.gio.javafx;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.expeditee.core.Font;
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+import org.expeditee.core.TextHitInfo;
+import org.expeditee.core.TextLayout;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.gio.TextLayoutManager;
+
+import javafx.geometry.Bounds;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextAlignment;
+
+public class JavaFXTextLayoutManager extends TextLayoutManager {
+	
+	/** Singleton instance. */
+	private static JavaFXTextLayoutManager _instance;
+	
+	/** Singleton instantiator. */
+	public static JavaFXTextLayoutManager getInstance()
+	{
+		if (_instance == null) _instance = new JavaFXTextLayoutManager();
+		
+		return _instance;
+	}
+	
+	/** Mapping from Expeditee text-layout objects to JavaFX equivalents. */
+	private HashMap<Long, javafx.scene.text.Text> _layoutMap;
+	
+	private JavaFXTextLayoutManager()
+	{
+		_layoutMap = new HashMap<Long, javafx.scene.text.Text>();
+	}
+	
+	private synchronized void register(TextLayout layout, javafx.scene.text.Text jfxLayout)
+	{
+		if (layout == null || jfxLayout == null) return;
+		
+		_layoutMap.put(layout.getHandle(), jfxLayout);
+	}
+	
+	public synchronized javafx.scene.text.Text getInternalLayout(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return _layoutMap.get(layout.getHandle());
+	}
+	
+	public synchronized boolean isTextLayoutValid(TextLayout layout)
+	{
+		if (layout == null) return false;
+		
+		return _layoutMap.containsKey(layout.getHandle());
+	}
+
+	@Override
+	public float getAdvance(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return Float.NaN;
+		
+		Text text = getInternalLayout(layout);
+		
+		Bounds textBounds = text.getBoundsInLocal();
+		
+		return (float) textBounds.getWidth();
+	}
+
+	@Override
+	public int getCharacterCount(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return 0;
+	
+		Text text = getInternalLayout(layout);
+		
+		return text.getText().length();
+	}
+
+	@Override
+	public TextHitInfo getNextLeftHit(TextLayout layout, int offset)
+	{
+		// TODO: Complete. cts16
+		return new TextHitInfo(offset, false);
+	}
+
+	@Override
+	public TextHitInfo getNextLeftHit(TextLayout layout, TextHitInfo hit)
+	{
+		// TODO: Complete. cts16
+		return hit;
+	}
+
+	@Override
+	public TextHitInfo getNextRightHit(TextLayout layout, int offset)
+	{
+		// TODO: Complete. cts16
+		return new TextHitInfo(offset, false);
+	}
+
+	@Override
+	public TextHitInfo getNextRightHit(TextLayout layout, TextHitInfo hit)
+	{
+		// TODO: Complete. cts16
+		return hit;
+	}
+
+	@Override
+	public float[] getCaretInfo(TextLayout layout, TextHitInfo hit)
+	{
+		// TODO: Complete. cts16
+		float[] ret = new float[2];
+		ret[0] = 0;
+		ret[1] = 1;
+		return ret;
+	}
+
+	@Override
+	public TextHitInfo hitTestChar(TextLayout layout, float x, float y)
+	{
+		// TODO: Complete. cts16
+		return new TextHitInfo(0, false);
+	}
+
+	@Override
+	public AxisAlignedBoxBounds getPixelBounds(TextLayout layout, float x, float y)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		Text text = getInternalLayout(layout);
+		
+		Bounds textBounds = text.getBoundsInLocal();
+		
+		return new AxisAlignedBoxBounds((int) x, (int) y, (int) textBounds.getWidth(), (int) textBounds.getHeight());
+	}
+
+	@Override
+	public AxisAlignedBoxBounds getLogicalHighlightShape(TextLayout layout, int firstEndpoint, int secondEndpoint)
+	{
+		// TODO: Complete. cts16
+		return getPixelBounds(layout, 0, -layout.getAscent());
+	}
+
+	@Override
+	public float getAscent(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return Float.NaN;
+		
+		Text text = getInternalLayout(layout);
+		
+		return (int) text.getBaselineOffset();
+	}
+
+	@Override
+	public float getDescent(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return Float.NaN;
+		
+		Text text = getInternalLayout(layout);
+		
+		return (int) (text.getBoundsInLocal().getHeight() - text.getBaselineOffset());
+	}
+
+	@Override
+	public float getLeading(TextLayout layout)
+	{
+		// TODO Auto-generated method stub
+		return 0;
+	}
+
+	// TODO: Remove magic constant of -1. cts16
+	@Override
+	public int getStringWidth(Font font, String string)
+	{
+		if (font == null || string == null) return -1;
+
+		// Make sure we have a JavaFX font to use
+		javafx.scene.text.Font jfxFont;
+		JavaFXFontManager fontManager = JavaFXMiscManager.getIfUsingJavaFXFontManager();
+		if (fontManager == null) return -1;
+		jfxFont = fontManager.getInternalFont(font);
+		if (jfxFont == null) {
+			JavaFXGraphicsManager g = JavaFXMiscManager.getIfUsingJavaFXGraphicsManager();
+			if (g == null) return -1;
+			jfxFont = fontManager.getInternalFont(fontManager.getDefaultFont());
+		}
+		if (jfxFont == null) return -1;
+		
+		// Create a text object to layout the text
+		javafx.scene.text.Text text = new javafx.scene.text.Text(string);
+		text.setFont(jfxFont);
+		
+		// Return the width of the text
+		return (int) text.getBoundsInLocal().getWidth();
+	}
+
+	@Override
+	public void releaseLayout(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return;
+		
+		_layoutMap.remove(layout.getHandle());
+	}
+
+	@Override
+	public List<TextLayout> layoutString(String string, Font font, Point start, Line[] lineBreakers, int widthLimit,
+			int lineSpacing, boolean dontBreakWords, boolean fullJustify)
+	{
+		if (string == null || font == null || start == null) return null;
+		
+		// Make sure we have a JavaFX font to use
+		javafx.scene.text.Font jfxFont;
+		JavaFXFontManager fontManager = JavaFXMiscManager.getIfUsingJavaFXFontManager();
+		if (fontManager == null) return null;
+		jfxFont = fontManager.getInternalFont(font);
+		if (jfxFont == null) return null;
+		
+		// Temporary list to accumulate the Texts in to
+		List<javafx.scene.text.Text> layouts = new LinkedList<javafx.scene.text.Text>();
+		List<Integer> offsets = new LinkedList<Integer>();
+		offsets.add(0);
+		List<Point> positions = new LinkedList<Point>();
+		positions.add(new Point(start));
+		
+		int startIndex = 0;
+		while (startIndex < string.length()) {
+			
+			int width = widthLimit;
+			if (lineBreakers != null) width = Math.min(width, (int) getLineWidth(start, lineBreakers));
+			
+			int endIndex = string.length();
+			for (int i = startIndex + 1; i < string.length(); i++) {
+				if (string.charAt(i) == '\n') {
+					endIndex = i;
+					break;
+				}
+			}
+			
+			javafx.scene.text.Text text = findTextNoWiderThan(string.substring(startIndex, endIndex), jfxFont, width, dontBreakWords);
+			
+			// If it's impossible to layout any more text without breaking a word, just do it
+			if (text == null || text.getText().length() == 0 && width == widthLimit) {
+				text = findTextNoWiderThan(string.substring(startIndex, endIndex), jfxFont, width, false);
+				// If still impossible, give up
+				if (text == null || text.getText().length() == 0) break;
+			}
+			
+			endIndex = startIndex + text.getText().length();
+			text.setX(start.x);
+			text.setY(start.y);
+
+			if (fullJustify && endIndex < string.length()) {
+				text = getJustifiedLayout(text, width);
+			}
+			
+			layouts.add(text);
+			offsets.add(endIndex);
+			startIndex = endIndex;
+			if (startIndex < string.length() && string.charAt(startIndex) == '\n') startIndex += 1;
+			int lineDrop = (int) (text.getBoundsInLocal().getHeight());
+			if (lineSpacing >= 0) {
+				lineDrop += lineSpacing; 
+			}
+			start.y += lineDrop;
+			positions.add(new Point(start));
+		}
+		
+		// Convert the accumulated list into an internal-type array
+		List<TextLayout> ret = new LinkedList<TextLayout>();
+		for (int i = 0; i < layouts.size(); i++) {
+			javafx.scene.text.Text text = layouts.get(i);
+			int startCharIndex = offsets.get(i);
+			int endCharIndex = offsets.get(i + 1) - 1;
+			String line = string.substring(startCharIndex, endCharIndex + 1);
+			TextLayout layout = TextLayout.get(line, font, startCharIndex, endCharIndex);
+			register(layout, text);
+			ret.add(layout);
+		}
+
+		return ret;
+	}
+	
+	private javafx.scene.text.Text findTextNoWiderThan(String string, javafx.scene.text.Font font, int maxWidth, boolean dontBreakWords)
+	{
+		if (string == null || font == null || maxWidth <= 0) return null;
+		
+		javafx.scene.text.Text text = new javafx.scene.text.Text(string);
+		text.setFont(font);
+		
+		// TODO: Change to binary search. cts16
+		while (text.getBoundsInLocal().getWidth() > maxWidth && string.length() > 0) {
+			string = string.substring(0, string.length() - 1);
+			if (dontBreakWords && !string.endsWith(" ")) continue;
+			text.setText(string);
+		}
+		
+		return text;
+	}
+	
+	private javafx.scene.text.Text getJustifiedLayout(javafx.scene.text.Text text, int width)
+	{
+		if (text == null) return null;
+		
+		text.setTextAlignment(TextAlignment.JUSTIFY);
+		text.setWrappingWidth(width);
+		
+		return text;
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/swing/AbsoluteLayout.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/AbsoluteLayout.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/AbsoluteLayout.java	(revision 1097)
@@ -0,0 +1,41 @@
+/**
+ *    AbsoluteLayout.java
+ *    Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
+ *
+ *    This program is free software: you can redistribute it and/or modify
+ *    it under the terms of the GNU General Public License as published by
+ *    the Free Software Foundation, either version 3 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.expeditee.gio.swing;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.LayoutManager;
+
+public class AbsoluteLayout implements LayoutManager {
+
+	public void addLayoutComponent(String name, Component comp) {}
+	public void removeLayoutComponent(Component comp) {}
+	
+	public Dimension minimumLayoutSize(Container parent) {
+		return parent.getMinimumSize();
+	}
+
+	public Dimension preferredLayoutSize(Container parent) {
+		return parent.getPreferredSize();
+	}
+
+	public void layoutContainer(Container parent) {}
+
+}
Index: /trunk/src/org/expeditee/gio/swing/ExpediteeRepaintManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/ExpediteeRepaintManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/ExpediteeRepaintManager.java	(revision 1097)
@@ -0,0 +1,116 @@
+/**
+ *    ExpediteeRepaintManager.java
+ *    Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
+ *
+ *    This program is free software: you can redistribute it and/or modify
+ *    it under the terms of the GNU General Public License as published by
+ *    the Free Software Foundation, either version 3 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.expeditee.gio.swing;
+
+import java.awt.Component;
+import java.awt.Point;
+import java.awt.Rectangle;
+
+import javax.swing.JComponent;
+import javax.swing.RepaintManager;
+
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayController;
+
+public class ExpediteeRepaintManager extends RepaintManager {
+	
+	private static ExpediteeRepaintManager _instance = null;
+	
+	public static ExpediteeRepaintManager getInstance()
+	{
+		if (_instance == null) _instance = new ExpediteeRepaintManager();
+		
+		return _instance;
+	}
+	
+	private ExpediteeRepaintManager()
+	{
+		super();
+		setDoubleBufferingEnabled(false); // use as few resources as possible
+	}
+	
+	
+    public synchronized void addDirtyRegion(JComponent c, int x, int y, int w, int h)
+    {
+    	if (c != null) {
+    		
+    		if (Browser._theBrowser == null || c == SwingMiscManager.getIfUsingSwingGraphicsManager().getContentPane()) return;
+
+    		Rectangle dirty = new Rectangle();
+    		convertRectangleOrigin(c, new Rectangle(x, y, w, h), dirty);
+			DisplayController.invalidateArea(SwingConversions.fromSwingRectangle(dirty));
+			DisplayController.requestRefresh(true); // ALWAYS REQUEST: Avoids AWT locks
+    		
+    	/*	if (convertRectangleOrigin(c, new Rectangle(x, y, w, h), dirty)) {
+        		FrameGraphics.invalidateArea(dirty);
+        		FrameGraphics.refresh(true);
+    		} else {
+    			FrameGraphics.invalidateArea(dirty);
+    			FrameGraphics.requestRefresh(true);
+    		}*/
+
+    		// DEBUGGING
+    		//Browser._theBrowser.g.setColor(Color.ORANGE);
+    		//Browser._theBrowser.g.fillRect(dirty.x, dirty.y, dirty.width, dirty.height);
+    		
+    	}
+    }
+    
+	/*@Override
+	public void addDirtyRegion(Applet applet, int x, int y, int w, int h) {
+		// Ignore
+	}
+
+	@Override
+	public void addDirtyRegion(Window window, int x, int y, int w, int h) {
+		// Ignore
+	}*/
+    
+    /**
+     * Converts a rectangle from swing/awt space to expeditee space
+     * @param c
+     * @param dirty
+     * @return
+     */
+    private boolean convertRectangleOrigin(JComponent c, Rectangle dirty, Rectangle dest)
+    {
+    	Point p = new Point(0,0);
+    	boolean isInContentPane = getPointInContentPane(c, p);
+    	dest.setBounds(dirty);
+    	dest.translate(p.x, p.y);
+    	return isInContentPane;
+    }
+    
+    private boolean getPointInContentPane(Component c, Point p)
+    {
+    	if (c == SwingMiscManager.getIfUsingSwingGraphicsManager().getContentPane()) {
+    		return true;
+    	} else if (c == null || c == SwingMiscManager.getIfUsingSwingGraphicsManager().getLayeredPane()) {
+    		return false;
+    	}
+
+    	p.translate(c.getX(), c.getY());
+    	
+    	return getPointInContentPane(c.getParent(), p);
+    }
+    
+
+
+
+}
Index: /trunk/src/org/expeditee/gio/swing/MouseEventRouter.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/MouseEventRouter.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/MouseEventRouter.java	(revision 1097)
@@ -0,0 +1,387 @@
+/**
+ *    MouseEventRouter.java
+ *    Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
+ *
+ *    This program is free software: you can redistribute it and/or modify
+ *    it under the terms of the GNU General Public License as published by
+ *    the Free Software Foundation, either version 3 of the License, or
+ *    (at your option) any later version.
+ *
+ *    This program is distributed in the hope that it will be useful,
+ *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *    GNU General Public License for more details.
+ *
+ *    You should have received a copy of the GNU General Public License
+ *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.expeditee.gio.swing;
+
+import java.awt.AWTEvent;
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.awt.event.AWTEventListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JComponent;
+import javax.swing.JMenuBar;
+import javax.swing.JPopupMenu;
+import javax.swing.SwingUtilities;
+
+import org.expeditee.gio.gesture.StandardGestureActions;
+import org.expeditee.gui.DisplayController;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameUtils;
+import org.expeditee.gui.FreeItems;
+import org.expeditee.gui.PopupManager;
+import org.expeditee.items.Item;
+
+/**
+ * The gateway for mouse input; conditionally forwards mouse messages to swing
+ * components / expeditee frames for the Browser.
+ * 
+ * Various mouse listeners can attach themselves here to listen to mouse events
+ * forwarded to expeditee .. this excludes widget-exclusive mouse messages.
+ * 
+ * @author Brook Novak
+ * 
+ */
+public class MouseEventRouter extends JComponent {
+
+	private static final long serialVersionUID = 1L;
+
+	private JMenuBar _menuBar;
+
+	private Container _contentPane;
+
+	private List<MouseListener> _mouseListeners = new LinkedList<MouseListener>();
+
+	private List<MouseMotionListener> _mouseMotionListeners = new LinkedList<MouseMotionListener>();
+
+	private List<MouseWheelListener> _mouseWheelListeners = new LinkedList<MouseWheelListener>();
+
+	private static MouseEvent _currentMouseEvent = null;
+
+	/**
+	 * Routes events on given frame layers... the menu bar and content pane
+	 * 
+	 * @param menuBar
+	 *            Must not be null.
+	 * 
+	 * @param contentPane
+	 *            Must not be null.
+	 */
+	public MouseEventRouter(JMenuBar menuBar, Container contentPane)
+	{
+		if (contentPane == null) throw new NullPointerException("contentPane");
+
+		// Listen for all AWT events (ensures popups are included)
+		Toolkit.getDefaultToolkit().addAWTEventListener(new EventCatcher(),
+				AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_WHEEL_EVENT_MASK);
+
+		this._menuBar = menuBar;
+		this._contentPane = contentPane;
+	}
+
+	/**
+	 * Listens only to events to frames... i.e. to expeditee, not to widgets.
+	 * 
+	 * @param listener
+	 *            The listener to add.
+	 */
+	public void addExpediteeMouseListener(MouseListener listener)
+	{
+		if (listener == null) throw new NullPointerException("listener");
+		
+		_mouseListeners.add(listener);
+	}
+
+	public void removeExpediteeMouseListener(MouseListener listener)
+	{
+		if (listener == null) throw new NullPointerException("listener");
+		
+		_mouseListeners.remove(listener);
+	}
+
+	public void addExpediteeMouseMotionListener(MouseMotionListener listener)
+	{
+		if (listener == null) throw new NullPointerException("listener");
+		
+		_mouseMotionListeners.add(listener);
+	}
+
+	public void removeExpediteeMouseMotionListener(MouseMotionListener listener)
+	{
+		if (listener == null) throw new NullPointerException("listener");
+		
+		_mouseMotionListeners.remove(listener);
+	}
+
+	public void addExpediteeMouseWheelListener(MouseWheelListener listener)
+	{
+		if (listener == null) throw new NullPointerException("listener");
+		
+		_mouseWheelListeners.add(listener);
+	}
+
+	public void removeExpediteeMouseWheelListener(MouseWheelListener listener)
+	{
+		if (listener == null) throw new NullPointerException("listener");
+		
+		_mouseWheelListeners.remove(listener);
+	}
+
+	/**
+	 * Conceal event catching from outside
+	 * 
+	 * @author Brook Novak
+	 */
+	private class EventCatcher implements AWTEventListener
+	{
+		/**
+		 * All events for every component in frame are fired to here
+		 */
+		public void eventDispatched(AWTEvent event) {
+			if (event instanceof MouseEvent) {
+				routeMouseEvent((MouseEvent) event);
+			}
+		}
+
+	}
+
+	/**
+	 * Forwards the mouse event to its appropriate destination. For events that
+	 * belong to Expeditee space the events are consumed and manually routed to
+	 * the mouse actions handler.
+	 * 
+	 * @param e
+	 *            The mouse event for any component in the browser frame
+	 */
+	private void routeMouseEvent(MouseEvent e)
+	{
+		_currentMouseEvent = e;
+
+		// First convert the point to expeditee space
+		Point containerPoint = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), _contentPane);
+		
+		// TODO: Find a reliable way of detecting when the mouse moved onto a window that isn't a child of ours
+		if(e.getID() == MouseEvent.MOUSE_EXITED) {
+			// System.out.println(e.getComponent());
+			if(containerPoint.x <= 0 || containerPoint.x >= _contentPane.getWidth() ||
+					containerPoint.y <= 0 || containerPoint.y >= _contentPane.getHeight())
+			{
+				//FrameGestureActions.mouseExitedWindow(e);
+			}
+		}
+
+		if (containerPoint.y < 0 && e.getID() != MouseEvent.MOUSE_EXITED) { // not in the content pane
+
+			if (_menuBar != null && containerPoint.y + _menuBar.getHeight() >= 0) {
+				// The mouse event is over the menu bar.
+				// Could handle specially.
+			} else {
+				// The mouse event is over non-system window
+				// decorations, such as the ones provided by
+				// the Java look and feel.
+				// Could handle specially.
+			}
+
+		} else {
+
+			// Check to see if the mouse is over an expeditee item or
+			// whether an expeditee item is currently picked up
+			boolean forwardToExpeditee = false;
+			boolean isOverPopup = PopupManager.getInstance().isPointOverPopup(SwingConversions.fromSwingPoint(containerPoint));
+			if (isOverPopup) {
+				// Popups have highest preference
+				// forwardToExpiditee = false...
+
+				// If there are items in free space - keep them moving with the
+				// cursor over the
+				// popups.
+				if (!FreeItems.getInstance().isEmpty()) {
+					StandardGestureActions.move(FreeItems.getInstance(), SwingConversions.fromSwingPoint(containerPoint));
+				}
+
+				// Note: all frame.content pane events belong to expeditee
+			} else if (e.getSource() == _contentPane || e.getSource() == SwingMiscManager.getIfUsingSwingGraphicsManager().getJFrame() || !FreeItems.getInstance().isEmpty())
+			{
+				forwardToExpeditee = true;
+			} else if (DisplayController.getCurrentFrame() != null) {
+				/* is mouse over a specific expeditee item? */
+				// NOTE: Do not use FrameUtils.getCurrentItem() - thats relevent
+				// for old mouse position only
+				/*
+				 * for (Item i : DisplayIO.getCurrentFrame().getItems()) { if
+				 * (i.getPolygon().contains(containerPoint)) {
+				 * forwardToExpiditee = true; break; } }
+				 */// ABOVE: Does not consider overlays
+				// NOTE: Below is an expensive operation and could be re-used
+				// when passing mouse events!!!
+				forwardToExpeditee = (FrameUtils.onItem(DisplayController.getCurrentFrame(), containerPoint.x, containerPoint.y, true) != null);
+			} else {
+				forwardToExpeditee = false;
+			}
+
+			// Create artificial mouse event and forward it to expeditee
+			MouseEvent expediteeEvent = SwingUtilities.convertMouseEvent(e.getComponent(), e, _contentPane);
+
+			// NOTE: Convert util masks-out the needed extensions
+			MouseEvent withExtensions = null;
+
+			if (forwardToExpeditee) {
+
+				// Ensure that underlying widgets don't get the event
+				e.consume();
+
+				switch (e.getID()) {
+				case MouseEvent.MOUSE_MOVED:
+
+					for (MouseMotionListener listener : _mouseMotionListeners) {
+						listener.mouseMoved(expediteeEvent);
+					}
+
+					// Ensure that expeditee has focus only if no pop-ups exist
+					SwingGraphicsManager g = SwingMiscManager.getIfUsingSwingGraphicsManager();
+					if (g.getContentPane() != null) {
+						if (!g.getContentPane().isFocusOwner() && !isPopupVisible()) {
+							g.getContentPane().requestFocus();
+						}
+					}
+
+					break;
+
+				case MouseEvent.MOUSE_CLICKED:
+
+					withExtensions = duplicateMouseEvent(expediteeEvent, e.getModifiers() | e.getModifiersEx());
+
+					for (MouseListener listener : _mouseListeners) {
+						listener.mouseClicked(withExtensions);
+					}
+
+					break;
+
+				case MouseEvent.MOUSE_PRESSED:
+
+					withExtensions = duplicateMouseEvent(expediteeEvent, e.getModifiers() | e.getModifiersEx());
+
+					for (MouseListener listener : _mouseListeners) {
+						listener.mousePressed(withExtensions);
+					}
+
+					break;
+
+				case MouseEvent.MOUSE_RELEASED:
+					
+					withExtensions = duplicateMouseEvent(expediteeEvent, e.getModifiers() | e.getModifiersEx());
+
+					for (MouseListener listener : _mouseListeners) {
+						listener.mouseReleased(withExtensions);
+					}
+
+					break;
+				case MouseEvent.MOUSE_WHEEL:
+					MouseWheelEvent mwe = (MouseWheelEvent) expediteeEvent;
+					for (MouseWheelListener listener : _mouseWheelListeners) {
+						listener.mouseWheelMoved(mwe);
+					}
+					break;
+				case MouseEvent.MOUSE_ENTERED:
+					withExtensions = duplicateMouseEvent(expediteeEvent, e.getModifiers() | e.getModifiersEx());
+					for (MouseListener listener : _mouseListeners) {
+						listener.mouseEntered(withExtensions);
+					}
+					break;
+				case MouseEvent.MOUSE_EXITED:
+					withExtensions = duplicateMouseEvent(expediteeEvent, e.getModifiers() | e.getModifiersEx());
+					for (MouseListener listener : _mouseListeners) {
+						listener.mouseExited(withExtensions);
+					}
+					break;
+				case MouseEvent.MOUSE_DRAGGED:
+					for (MouseMotionListener listener : _mouseMotionListeners) {
+						listener.mouseDragged(expediteeEvent);
+					}
+					break;
+				}
+
+			} else {
+
+				// Keep expeditees mouse X/Y updated
+				//FrameMouseActions.MouseX = expediteeEvent.getX();
+				//FrameMouseActions.MouseY = expediteeEvent.getY();
+
+				// If forwarding to swing ensure that widgets are not highlighted
+				// to give visual feedback to users such that swing has focus.
+				Item i = StandardGestureActions.getlastHighlightedItem();
+				if (i != null && i.getHighlightMode() != Item.HighlightMode.None) {
+					FrameGraphics.changeHighlightMode(i, Item.HighlightMode.None);
+				}
+
+				// Also bring expeditee behaviour to swing: Auto-focus on component
+				// whenever the mouse moves over it.
+				if (e.getID() == MouseEvent.MOUSE_MOVED) {
+					if (e.getSource() instanceof Component) {
+						Component target = (Component) e.getSource();
+						if (!target.isFocusOwner()) {
+							target.requestFocus();
+						}
+					}
+					// Auto-hide pop-ups when user click on something other
+					// than a pop-up - and is not on a pop-up invoker
+				} /*else if (!isOverPopup // TODO: Reinstate. cts16
+						&& e.getID() == MouseEvent.MOUSE_PRESSED
+						&& !PopupManager.getInstance().isInvoker(e.getComponent()))
+				{
+					PopupManager.getInstance().hideAutohidePopups();
+				}*/
+			}
+		}
+	}
+
+	public static boolean isPopupVisible()
+	{
+		return isPopupVisible(SwingMiscManager.getIfUsingSwingGraphicsManager().getLayeredPane());
+	}
+
+	private static boolean isPopupVisible(Container parent)
+	{
+		for (Component c : parent.getComponents()) {
+
+			if (c instanceof JPopupMenu && ((JPopupMenu) c).isVisible()) {
+				return true;
+
+			} else if (c instanceof Container && c != SwingMiscManager.getIfUsingSwingGraphicsManager().getContentPane()) {
+				if (isPopupVisible((Container) c))
+					return true;
+			}
+		}
+
+		return false;
+	}
+
+	private MouseEvent duplicateMouseEvent(MouseEvent e, int modifiers)
+	{
+		return new MouseEvent(e.getComponent(), e.getID(), e.getWhen(),
+				modifiers, e.getX(), e.getY(),
+				/** The below methods are not compatible with Java 1.5 */
+				/*
+				 * e.getXOnScreen(), e.getYOnScreen(),
+				 */
+				e.getClickCount(), e.isPopupTrigger(), e.getButton());
+	}
+
+	public static MouseEvent getCurrentMouseEvent()
+	{
+		return _currentMouseEvent;
+	}
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingClipboardManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingClipboardManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingClipboardManager.java	(revision 1097)
@@ -0,0 +1,189 @@
+package org.expeditee.gio.swing;
+
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.expeditee.core.Image;
+import org.expeditee.gio.ClipboardManager;
+
+/** TODO: Comment. cts16 */
+public class SwingClipboardManager extends ClipboardManager {
+
+	public static final DataFlavor expediteeDataFlavour = new DataFlavor(Object.class, "expediteeDataFlavour");
+	
+	// The return flavour of data in preferential order
+	public static final DataFlavor[] flavours = {
+			SwingClipboardManager.expediteeDataFlavour,
+			DataFlavor.imageFlavor,
+			DataFlavor.stringFlavor
+	};
+	
+	private static SwingClipboardManager _instance;
+	
+	public static SwingClipboardManager getInstance()
+	{
+		if (_instance == null) {
+			_instance = new SwingClipboardManager();
+		}
+		
+		return _instance;
+	}
+	
+	@Override
+	public void set(ClipboardData data)
+	{
+		if (data == null) return;
+		
+		SwingClipboardGeneralTransferable t = new SwingClipboardGeneralTransferable(data.data);
+		t.addImageRepresentation(SwingMiscManager.getIfUsingSwingImageManager().getBufferedImageForImage(data.imageRepresentation));
+		t.addStringRepresentation(data.stringRepresentation);
+		
+		Toolkit.getDefaultToolkit().getSystemClipboard().setContents(t, null);
+	}
+
+	@Override
+	public ClipboardData get()
+	{
+		Transferable content = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
+		
+		// No clipboard contents
+		if (content == null) return null;
+		
+		ClipboardData ret = new ClipboardData();
+		
+		// Try each flavour
+		for (DataFlavor flavour : flavours) {
+			if (content.isDataFlavorSupported(flavour)) {
+				try {
+					handleGet(flavour, content, ret);
+				} catch (IOException e) {
+					// Data stale, just move on to next flavour
+				} catch (UnsupportedFlavorException e) {
+					// Shouldn't get here because we just checked, but just move on anyway
+				}
+			}
+		}
+		
+		// No supported content
+		return ret;
+	}
+	
+	private void handleGet(DataFlavor flavour, Transferable content, ClipboardData out) throws IOException, UnsupportedFlavorException
+	{
+		if (expediteeDataFlavour.equals(flavour)) {
+			out.data = content.getTransferData(flavour);
+		} else if (DataFlavor.imageFlavor.equals(flavour)) {
+			out.imageRepresentation = handleGetImage(content);
+		} else if (DataFlavor.stringFlavor.equals(flavour)) {
+			out.stringRepresentation = (String) content.getTransferData(flavour);
+		}
+	}
+	
+	private Image handleGetImage(Transferable content) throws IOException, UnsupportedFlavorException
+	{
+		Object o = content.getTransferData(DataFlavor.imageFlavor);
+		
+		SwingImageManager manager = SwingMiscManager.getIfUsingSwingImageManager();
+		
+		if (manager == null) return null;
+		
+		return manager.createImage((java.awt.Image) o);
+	}
+
+	/** TODO: Comment. cts16 */
+	private class SwingClipboardGeneralTransferable implements Transferable
+	{
+		Object _data;
+		java.awt.Image _imageRep;
+		String _stringRep;
+		
+		private List<DataFlavor> _flavours;
+		
+		@SuppressWarnings("unused")
+		public SwingClipboardGeneralTransferable()
+		{
+			this(null);
+		}
+		
+		public SwingClipboardGeneralTransferable(Object data)
+		{
+			_flavours = new LinkedList<DataFlavor>();
+			
+			addGenericRepresentation(data);
+		}
+		
+		public void addGenericRepresentation(Object data)
+		{
+			if (data == null) return;
+			
+			_data = data;
+			addDataFlavour(SwingClipboardManager.expediteeDataFlavour);
+		}
+		
+		public void addImageRepresentation(java.awt.Image image)
+		{
+			if (image == null) return;
+			
+			_imageRep = image;
+			addDataFlavour(DataFlavor.imageFlavor);
+		}
+		
+		public void addStringRepresentation(String string)
+		{
+			if (string == null) return;
+			
+			_stringRep = string;
+			addDataFlavour(DataFlavor.stringFlavor);
+		}
+
+		public void addDataFlavour(DataFlavor flavour)
+		{
+			_flavours.add(flavour);
+		}
+		
+		@Override
+		public DataFlavor[] getTransferDataFlavors()
+		{
+			DataFlavor[] ret = new DataFlavor[_flavours.size()];
+			int i = 0;
+			for (DataFlavor flavour : _flavours) {
+				ret[i] = flavour;
+				i++;
+			}
+			return ret;
+		}
+
+		@Override
+		public boolean isDataFlavorSupported(DataFlavor flavor)
+		{
+			for (DataFlavor flavour : _flavours) {
+				if (flavour.equals(flavor)) return true;
+			}
+			return false;
+		}
+
+		@Override
+		public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
+		{
+			if (isDataFlavorSupported(flavor)) {
+				if (flavor.equals(DataFlavor.imageFlavor)) {
+					return _imageRep;
+				} else if (flavor.equals(DataFlavor.stringFlavor)) {
+					return _stringRep;
+				} else {
+					return _data;
+				}
+			} else {
+				throw new UnsupportedFlavorException(flavor);
+			}
+		}
+		
+	}
+	
+	
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingConversions.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingConversions.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingConversions.java	(revision 1097)
@@ -0,0 +1,355 @@
+package org.expeditee.gio.swing;
+
+public class SwingConversions {
+	
+	public static org.expeditee.core.Dimension fromSwingDimension(java.awt.Dimension dimension)
+	{
+		if (dimension == null) return null;
+		
+		return new org.expeditee.core.Dimension(dimension.width, dimension.height);
+	}
+	
+	public static java.awt.Dimension toSwingDimension(org.expeditee.core.Dimension dimension)
+	{
+		if (dimension == null) return null;
+		
+		return new java.awt.Dimension(dimension.width, dimension.height);
+	}
+	
+	public static java.awt.Color toSwingColor(org.expeditee.core.Colour c)
+	{
+		if (c == null) return null;
+		
+		int r = c.getRed255();
+		int g = c.getGreen255();
+		int b = c.getBlue255();
+		int a = c.getAlpha255();
+		
+		return new java.awt.Color(r, g, b, a);
+	}
+	
+	public static org.expeditee.core.Colour fromSwingColor(java.awt.Color c)
+	{
+		if (c == null) return null;
+		
+		return org.expeditee.core.Colour.FromRGBA255(c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha());
+	}
+	
+	public static java.awt.Point toSwingPoint(org.expeditee.core.Point point)
+	{
+		if (point == null) return null;
+		
+		return new java.awt.Point(point.x, point.y);
+	}
+	
+	public static org.expeditee.core.Point fromSwingPoint(java.awt.Point point)
+	{
+		if (point == null) return null;
+		
+		return new org.expeditee.core.Point(point.x, point.y);
+	}
+	
+	public static org.expeditee.core.Point fromSwingPoint(java.awt.geom.Point2D point)
+	{
+		if (point == null) return null;
+		
+		return new org.expeditee.core.Point((int) point.getX(), (int) point.getY());
+	}
+	
+	public static java.awt.Rectangle toSwingRectangle(org.expeditee.core.bounds.AxisAlignedBoxBounds b)
+	{
+		if (b == null) return null;
+		
+		return new java.awt.Rectangle(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight());
+	}
+	
+	public static org.expeditee.core.bounds.AxisAlignedBoxBounds fromSwingRectangle(java.awt.Rectangle r)
+	{
+		if (r == null) return null;
+		
+		return new org.expeditee.core.bounds.AxisAlignedBoxBounds(r.x, r.y, r.width, r.height);
+	}
+	
+	public static java.awt.font.TextHitInfo toSwingTextHitInfo(org.expeditee.core.TextHitInfo thi)
+	{
+		if (thi == null) return null;
+		
+		if (thi.isLeadingEdge) {
+			return java.awt.font.TextHitInfo.leading(thi.charIndex);
+		} else {
+			return java.awt.font.TextHitInfo.trailing(thi.charIndex);
+		}
+	}
+
+	public static org.expeditee.core.TextHitInfo fromSwingTextHitInfo(java.awt.font.TextHitInfo swingThi)
+	{
+		if (swingThi == null) return null;
+		
+		return new org.expeditee.core.TextHitInfo(swingThi.getCharIndex(), swingThi.isLeadingEdge());
+	}
+	
+	public static int toSwingFontStyle(org.expeditee.core.Font.Style style)
+	{
+		switch (style) {
+		case PLAIN:
+			return java.awt.Font.PLAIN;
+		case BOLD:
+			return java.awt.Font.BOLD;
+		case ITALIC:
+			return java.awt.Font.ITALIC;
+		case BOLD_ITALIC:
+			return java.awt.Font.BOLD | java.awt.Font.ITALIC;
+		default:
+			return -1;	
+		}
+	}
+	
+	public static org.expeditee.core.Font.Style fromSwingFontStyle(int style)
+	{
+		switch (style) {
+		case java.awt.Font.BOLD | java.awt.Font.ITALIC:
+			return org.expeditee.core.Font.Style.BOLD_ITALIC;
+		case java.awt.Font.BOLD:
+			return org.expeditee.core.Font.Style.BOLD;
+		case java.awt.Font.ITALIC:
+			return org.expeditee.core.Font.Style.ITALIC;
+		case java.awt.Font.PLAIN:
+			return org.expeditee.core.Font.Style.PLAIN;
+		default:
+			return null;
+		}
+	}
+	
+	public static int toSwingCursorType(org.expeditee.core.Cursor.CursorType type)
+	{
+		switch (type) {
+			case DEFAULT:
+				return java.awt.Cursor.DEFAULT_CURSOR;
+			case TEXT:
+				return java.awt.Cursor.TEXT_CURSOR;
+			case CROSSHAIR:
+				return java.awt.Cursor.CROSSHAIR_CURSOR;
+			default:
+				return java.awt.Cursor.CUSTOM_CURSOR;
+		}
+	}
+	
+	public static org.expeditee.core.Cursor.CursorType fromSwingCursorType(int type)
+	{
+		switch (type) {
+		case java.awt.Cursor.DEFAULT_CURSOR:
+			return org.expeditee.core.Cursor.CursorType.DEFAULT;
+		case java.awt.Cursor.TEXT_CURSOR:
+			return org.expeditee.core.Cursor.CursorType.TEXT;
+		case java.awt.Cursor.CROSSHAIR_CURSOR:
+			return org.expeditee.core.Cursor.CursorType.CROSSHAIR;
+		case java.awt.Cursor.CUSTOM_CURSOR:
+			return org.expeditee.core.Cursor.CursorType.CUSTOM;
+		default:
+			return null;
+		}
+	}
+	
+	public static org.expeditee.core.GradientFill fromSwingGradientPaint(java.awt.GradientPaint paint)
+	{
+		if (paint == null) return null;
+		
+		org.expeditee.core.Colour colour1 = fromSwingColor(paint.getColor1());
+		org.expeditee.core.Point point1 = fromSwingPoint(paint.getPoint1());
+		org.expeditee.core.Colour colour2 = fromSwingColor(paint.getColor2());
+		org.expeditee.core.Point point2 = fromSwingPoint(paint.getPoint2());
+		
+		return new org.expeditee.core.GradientFill(colour1, point1, colour2, point2, paint.isCyclic());
+	}
+	
+	public static java.awt.GradientPaint toSwingGradientPaint(org.expeditee.core.GradientFill fill)
+	{
+		if (fill == null) return null;
+		
+		java.awt.Color color1 = toSwingColor(fill.getFromColour());
+		java.awt.Color color2 = toSwingColor(fill.getToColour());
+		
+		return new java.awt.GradientPaint(fill.getFromPoint().x, fill.getFromPoint().y, color1, fill.getToPoint().x, fill.getToPoint().y, color2, !fill.isAcyclic());
+	}
+	
+	public static org.expeditee.core.Stroke fromSwingStroke(java.awt.BasicStroke basicStroke)
+	{
+		if (basicStroke == null) return null;
+		
+		org.expeditee.core.Stroke stroke = new org.expeditee.core.Stroke();
+		stroke.thickness = basicStroke.getLineWidth();
+		stroke.capType = fromSwingCapType(basicStroke.getEndCap());
+		stroke.joinType = fromSwingJoinType(basicStroke.getLineJoin());
+		stroke.dashArray = basicStroke.getDashArray().clone();
+		stroke.dashPhase = basicStroke.getDashPhase();
+		
+		return stroke;
+	}
+	
+	public static java.awt.BasicStroke toSwingStroke(org.expeditee.core.Stroke stroke)
+	{
+		if (stroke == null) return null;
+		
+		java.awt.BasicStroke basicStroke = new java.awt.BasicStroke(stroke.thickness,
+																	toSwingCapType(stroke.capType),
+																	toSwingJoinType(stroke.joinType),
+																	10.0f, // This is the default for miterLimit
+																	stroke.dashArray.clone(),
+																	stroke.dashPhase);
+		
+		return basicStroke;
+	}
+	
+	public static org.expeditee.core.Stroke.CAP fromSwingCapType(int swingCapType)
+	{
+		switch(swingCapType) {
+			case java.awt.BasicStroke.CAP_BUTT:
+				return org.expeditee.core.Stroke.CAP.BUTT;
+			case java.awt.BasicStroke.CAP_ROUND:
+				return org.expeditee.core.Stroke.CAP.ROUND;
+			case java.awt.BasicStroke.CAP_SQUARE:
+				return org.expeditee.core.Stroke.CAP.SQUARE;
+			default:
+				return null;
+		}
+	}
+	
+	public static int toSwingCapType(org.expeditee.core.Stroke.CAP cap)
+	{
+		switch(cap) {
+			case BUTT:
+				return java.awt.BasicStroke.CAP_BUTT;
+			case ROUND:
+				return java.awt.BasicStroke.CAP_ROUND;
+			case SQUARE:
+				return java.awt.BasicStroke.CAP_SQUARE;
+			default:
+				return -1;
+		}
+	}
+	
+	public static org.expeditee.core.Stroke.JOIN fromSwingJoinType(int swingJoinType)
+	{
+		switch(swingJoinType) {
+			case java.awt.BasicStroke.JOIN_BEVEL:
+				return org.expeditee.core.Stroke.JOIN.BEVEL;
+			case java.awt.BasicStroke.JOIN_MITER:
+				return org.expeditee.core.Stroke.JOIN.MITER;
+			case java.awt.BasicStroke.JOIN_ROUND:
+				return org.expeditee.core.Stroke.JOIN.ROUND;
+			default:
+				return null;
+		}
+	}
+	
+	public static int toSwingJoinType(org.expeditee.core.Stroke.JOIN join)
+	{
+		switch(join) {
+			case BEVEL:
+				return java.awt.BasicStroke.JOIN_BEVEL;
+			case MITER:
+				return java.awt.BasicStroke.JOIN_MITER;
+			case ROUND:
+				return java.awt.BasicStroke.JOIN_ROUND;
+			default:
+				return -1;
+		}
+	}
+	
+	public static org.expeditee.core.bounds.PolygonBounds fromSwingPolygon(java.awt.Polygon polygon)
+	{
+		org.expeditee.core.bounds.PolygonBounds polygonBounds = new org.expeditee.core.bounds.PolygonBounds();
+		
+		for (int i = 0; i < polygon.npoints; i++) {
+			polygonBounds.addPoint(polygon.xpoints[i], polygon.ypoints[i]);
+		}
+		
+		return polygonBounds;
+	}
+	
+	public static java.awt.Polygon toSwingPolygon(org.expeditee.core.bounds.PolygonBounds polygonBounds)
+	{
+		java.awt.Polygon polygon = new java.awt.Polygon();
+		
+		for (int i = 0; i < polygonBounds.getPointCount(); i++) {
+			org.expeditee.core.Point p = polygonBounds.getPoint(i);
+			polygon.addPoint(p.x, p.y);
+		}
+		
+		return polygon;
+	}
+	
+	public static int toSwingMouseButton(org.expeditee.gio.input.KBMInputEvent.MouseButton mouseButton)
+	{
+		switch (mouseButton) {
+		case LEFT:
+			return java.awt.event.MouseEvent.BUTTON1;
+		case MIDDLE:
+			return java.awt.event.MouseEvent.BUTTON2;
+		case RIGHT:
+			return java.awt.event.MouseEvent.BUTTON3;
+		default:
+			return java.awt.event.MouseEvent.NOBUTTON;
+		}
+	}
+	
+	public static org.expeditee.gio.input.KBMInputEvent.Key fromSwingVirtualKey(int swingKey)
+	{
+		switch (swingKey) {
+		case java.awt.event.KeyEvent.VK_SHIFT: return org.expeditee.gio.input.KBMInputEvent.Key.SHIFT;
+		case java.awt.event.KeyEvent.VK_CONTROL: return org.expeditee.gio.input.KBMInputEvent.Key.CTRL;
+		case java.awt.event.KeyEvent.VK_ALT: return org.expeditee.gio.input.KBMInputEvent.Key.ALT;
+		case java.awt.event.KeyEvent.VK_ESCAPE: return org.expeditee.gio.input.KBMInputEvent.Key.ESC;
+		case java.awt.event.KeyEvent.VK_F1: return org.expeditee.gio.input.KBMInputEvent.Key.F1;
+		case java.awt.event.KeyEvent.VK_F2: return org.expeditee.gio.input.KBMInputEvent.Key.F2;
+		case java.awt.event.KeyEvent.VK_F3: return org.expeditee.gio.input.KBMInputEvent.Key.F3;
+		case java.awt.event.KeyEvent.VK_F4: return org.expeditee.gio.input.KBMInputEvent.Key.F4;
+		case java.awt.event.KeyEvent.VK_F5: return org.expeditee.gio.input.KBMInputEvent.Key.F5;
+		case java.awt.event.KeyEvent.VK_F6: return org.expeditee.gio.input.KBMInputEvent.Key.F6;
+		case java.awt.event.KeyEvent.VK_F7: return org.expeditee.gio.input.KBMInputEvent.Key.F7;
+		case java.awt.event.KeyEvent.VK_F8: return org.expeditee.gio.input.KBMInputEvent.Key.F8;
+		case java.awt.event.KeyEvent.VK_F9: return org.expeditee.gio.input.KBMInputEvent.Key.F9;
+		case java.awt.event.KeyEvent.VK_F10: return org.expeditee.gio.input.KBMInputEvent.Key.F10;
+		case java.awt.event.KeyEvent.VK_F11: return org.expeditee.gio.input.KBMInputEvent.Key.F11;
+		case java.awt.event.KeyEvent.VK_F12: return org.expeditee.gio.input.KBMInputEvent.Key.F12;
+		case java.awt.event.KeyEvent.VK_UP: return org.expeditee.gio.input.KBMInputEvent.Key.UP_ARROW;
+		case java.awt.event.KeyEvent.VK_DOWN: return org.expeditee.gio.input.KBMInputEvent.Key.DOWN_ARROW;
+		case java.awt.event.KeyEvent.VK_LEFT: return org.expeditee.gio.input.KBMInputEvent.Key.LEFT_ARROW;
+		case java.awt.event.KeyEvent.VK_RIGHT: return org.expeditee.gio.input.KBMInputEvent.Key.RIGHT_ARROW;
+		case java.awt.event.KeyEvent.VK_HOME: return org.expeditee.gio.input.KBMInputEvent.Key.HOME;
+		case java.awt.event.KeyEvent.VK_END: return org.expeditee.gio.input.KBMInputEvent.Key.END;
+		case java.awt.event.KeyEvent.VK_DELETE: return org.expeditee.gio.input.KBMInputEvent.Key.DELETE;
+		case java.awt.event.KeyEvent.VK_PAGE_UP: return org.expeditee.gio.input.KBMInputEvent.Key.PAGE_UP;
+		case java.awt.event.KeyEvent.VK_PAGE_DOWN: return org.expeditee.gio.input.KBMInputEvent.Key.PAGE_DOWN;
+		case java.awt.event.KeyEvent.VK_BACK_SPACE: return org.expeditee.gio.input.KBMInputEvent.Key.BACKSPACE;
+		case java.awt.event.KeyEvent.VK_TAB: return org.expeditee.gio.input.KBMInputEvent.Key.TAB;
+		case java.awt.event.KeyEvent.VK_A: return org.expeditee.gio.input.KBMInputEvent.Key.A;
+		case java.awt.event.KeyEvent.VK_B: return org.expeditee.gio.input.KBMInputEvent.Key.B;
+		case java.awt.event.KeyEvent.VK_C: return org.expeditee.gio.input.KBMInputEvent.Key.C;
+		case java.awt.event.KeyEvent.VK_D: return org.expeditee.gio.input.KBMInputEvent.Key.D;
+		case java.awt.event.KeyEvent.VK_E: return org.expeditee.gio.input.KBMInputEvent.Key.E;
+		case java.awt.event.KeyEvent.VK_F: return org.expeditee.gio.input.KBMInputEvent.Key.F;
+		case java.awt.event.KeyEvent.VK_G: return org.expeditee.gio.input.KBMInputEvent.Key.G;
+		case java.awt.event.KeyEvent.VK_H: return org.expeditee.gio.input.KBMInputEvent.Key.H;
+		case java.awt.event.KeyEvent.VK_I: return org.expeditee.gio.input.KBMInputEvent.Key.I;
+		case java.awt.event.KeyEvent.VK_J: return org.expeditee.gio.input.KBMInputEvent.Key.J;
+		case java.awt.event.KeyEvent.VK_K: return org.expeditee.gio.input.KBMInputEvent.Key.K;
+		case java.awt.event.KeyEvent.VK_L: return org.expeditee.gio.input.KBMInputEvent.Key.L;
+		case java.awt.event.KeyEvent.VK_M: return org.expeditee.gio.input.KBMInputEvent.Key.M;
+		case java.awt.event.KeyEvent.VK_N: return org.expeditee.gio.input.KBMInputEvent.Key.N;
+		case java.awt.event.KeyEvent.VK_O: return org.expeditee.gio.input.KBMInputEvent.Key.O;
+		case java.awt.event.KeyEvent.VK_P: return org.expeditee.gio.input.KBMInputEvent.Key.P;
+		case java.awt.event.KeyEvent.VK_Q: return org.expeditee.gio.input.KBMInputEvent.Key.Q;
+		case java.awt.event.KeyEvent.VK_R: return org.expeditee.gio.input.KBMInputEvent.Key.R;
+		case java.awt.event.KeyEvent.VK_S: return org.expeditee.gio.input.KBMInputEvent.Key.S;
+		case java.awt.event.KeyEvent.VK_T: return org.expeditee.gio.input.KBMInputEvent.Key.T;
+		case java.awt.event.KeyEvent.VK_U: return org.expeditee.gio.input.KBMInputEvent.Key.U;
+		case java.awt.event.KeyEvent.VK_V: return org.expeditee.gio.input.KBMInputEvent.Key.V;
+		case java.awt.event.KeyEvent.VK_W: return org.expeditee.gio.input.KBMInputEvent.Key.W;
+		case java.awt.event.KeyEvent.VK_X: return org.expeditee.gio.input.KBMInputEvent.Key.X;
+		case java.awt.event.KeyEvent.VK_Y: return org.expeditee.gio.input.KBMInputEvent.Key.Y;
+		case java.awt.event.KeyEvent.VK_Z: return org.expeditee.gio.input.KBMInputEvent.Key.Z;
+		default: return null;
+		}
+	}
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingDragAndDropManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingDragAndDropManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingDragAndDropManager.java	(revision 1097)
@@ -0,0 +1,168 @@
+package org.expeditee.gio.swing;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.TransferHandler;
+
+import org.expeditee.Util;
+import org.expeditee.core.Point;
+import org.expeditee.gio.DragAndDropManager;
+import org.expeditee.gui.DisplayController;
+import org.expeditee.gui.MessageBay;
+
+public class SwingDragAndDropManager extends DragAndDropManager {
+
+	/** Singleton instance. */
+	private static SwingDragAndDropManager _instance = null;
+	
+	/** Singleton instantiator. */
+	public static SwingDragAndDropManager getInstance()
+	{
+		if (_instance == null) _instance = new SwingDragAndDropManager();
+		
+		return _instance;
+	}
+
+	// GNOME and KDE desktops have a specialized way of DNDing files
+	private DataFlavor _URIListDataflavorString;
+
+	private DataFlavor _URIListDataflavorCharArray;
+	
+	private ExpediteeTransferHandler _transferHandler;
+	
+	private SwingDragAndDropManager()
+	{
+		// Wire up the transfer handler
+		if (Util.isMinimumVersion6()) {
+			_transferHandler = new ExpediteeTransferHandler();
+			SwingMiscManager.getIfUsingSwingGraphicsManager().setTransferHandler(_transferHandler);
+		} else {
+			System.err.println("Upgrade to a (minimum) of Java 1.6 to enable drag and drop support in Expeditee");
+		}
+		
+		try {
+			_URIListDataflavorString = new DataFlavor("text/uri-list;class=java.lang.String");
+			_URIListDataflavorCharArray = new DataFlavor("text/uri-list;class=\"[C\"");
+			
+		// This would never happen, java.lang.String is always present
+		} catch (ClassNotFoundException e) {
+			e.printStackTrace();
+			_URIListDataflavorString = null;
+		}
+		assert (_URIListDataflavorString != null);
+		assert (_URIListDataflavorCharArray != null);
+	}
+	
+	private class ExpediteeTransferHandler extends TransferHandler {
+		
+		/** Auto-generated serialisation ID#. */
+		private static final long serialVersionUID = 6552099431345591756L;
+
+		@Override
+		public boolean canImport(TransferSupport support) {
+
+			if (!support.isDrop()) {
+				return false;
+			}
+
+			// we only import Strings
+			if (	support.isDataFlavorSupported(DataFlavor.stringFlavor) ||
+					support.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ||
+					support.isDataFlavorSupported(_URIListDataflavorString) ||
+					support.isDataFlavorSupported(_URIListDataflavorCharArray))
+			{
+				// check if the source actions (a bitwise-OR of supported actions)
+				// contains the COPY action
+				boolean copySupported = (COPY & support.getSourceDropActions()) == COPY;
+				if (copySupported) {
+					support.setDropAction(COPY);
+					return true;
+				}
+			}
+
+			// Reject transfer
+			return false;
+		}
+		
+		@Override
+		public boolean importData(TransferSupport support)
+		{
+			if (!canImport(support) || DisplayController.getCurrentFrame() == null) return false;
+
+			// Get the location of where to drop the import
+			DropLocation location = support.getDropLocation();
+
+			// Convert it into expeditee space
+			Point expediteeDropPoint = SwingConversions.fromSwingPoint(location.getDropPoint());
+
+			try {
+
+				// The list of data flavors are ordered by most rich to least ..
+				// keep trying until first
+				// data flavor recognized.
+				DataFlavor[] flavors = support.getTransferable().getTransferDataFlavors();
+				for (DataFlavor df : flavors) {
+
+					System.out.println(df);
+
+					if (df == DataFlavor.stringFlavor) { // import as text item
+
+						String str = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
+
+						if (str != null && str.length() > 0) {
+							importString(str, expediteeDropPoint);
+							return true;
+						}
+
+					// Usually Windows and MAC enviroments
+					// Windows has other random types...
+					} else if (df == DataFlavor.javaFileListFlavor || df.getSubType().equals("x-java-file-list")) {
+
+						List<? extends File> files = (List<? extends File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
+
+						importFileList(files, expediteeDropPoint);
+
+						return true;
+
+					// Usually GNOME and KDE enviroments
+					} else if (df.equals(_URIListDataflavorString)) {
+
+						String data = (String) support.getTransferable().getTransferData(_URIListDataflavorString);
+
+						List<File> files = textURIListToFileList(data);
+
+						importFileList(files, expediteeDropPoint);
+
+						return true;
+
+					} else if (df.equals(_URIListDataflavorCharArray)) {
+
+						char[] data = (char[]) support.getTransferable().getTransferData(_URIListDataflavorCharArray);
+
+						String uriString = new String(data);
+
+						List<File> files = textURIListToFileList(uriString);
+
+						importFileList(files, expediteeDropPoint);
+
+						return true;
+					}
+				}
+
+			} catch (UnsupportedFlavorException e) {
+				MessageBay.displayMessage("Drag and drop for that type of data is not supported");
+			} catch (IOException e) {
+				e.printStackTrace();
+				MessageBay.displayMessage("Failed to import data in Expeditee");
+			}
+
+			return false;
+		}
+		
+	}
+
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingEcosystemManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingEcosystemManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingEcosystemManager.java	(revision 1097)
@@ -0,0 +1,18 @@
+package org.expeditee.gio.swing;
+
+import org.expeditee.gio.EcosystemManager;
+
+public class SwingEcosystemManager extends EcosystemManager
+{
+	public static void createSwingEcosystem()
+	{
+		setImageManager(SwingImageManager.getInstance());
+		setClipboardManager(SwingClipboardManager.getInstance());
+		setGraphicsManager(SwingGraphicsManager.getInstance());
+		setFontManager(SwingFontManager.getInstance());
+		setTextLayoutManager(SwingTextLayoutManager.getInstance());
+		setInputManager(SwingInputManager.getInstance());
+		setMiscManager(SwingMiscManager.getInstance());
+		setDragAndDropManager(SwingDragAndDropManager.getInstance());
+	}
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingFontManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingFontManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingFontManager.java	(revision 1097)
@@ -0,0 +1,173 @@
+package org.expeditee.gio.swing;
+
+import java.awt.GraphicsEnvironment;
+import java.io.File;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import org.expeditee.core.Font;
+import org.expeditee.gio.FontManager;
+
+/**
+ * The font manager for a Swing system.
+ * 
+ * @author cts16
+ */
+public class SwingFontManager extends FontManager {
+	
+	/** Singleton instance. */
+	private static SwingFontManager _instance = null;
+	
+	/** Singleton instantiator. */
+	public static SwingFontManager getInstance()
+	{
+		if (_instance == null) _instance = new SwingFontManager();
+		
+		return _instance;
+	}
+	
+	/** Mapping from handles to internal fonts. */
+	private HashMap<Font, java.awt.Font> _fontMap;
+	/** The font family names registered with the manager. */
+	private HashSet<String> _fontNames;
+	
+	/** Private constructor for singleton instance. */
+	private SwingFontManager()
+	{
+		// Initialise the font map
+		_fontMap = new HashMap<Font, java.awt.Font>();
+		
+		// Initialise the set of family names
+		_fontNames = new HashSet<String>();
+		
+		// Initialise default font to Swing default
+		setDefaultFont(register(java.awt.Font.decode(null)));
+		
+		// Add Swing standard fonts to registered fonts
+		registerSwingStandardFonts();// TODO: Reinstate. cts16
+	}
+
+	@Override
+	public Font registerFontFile(File fontFile)
+	{
+		if (fontFile == null) return null;
+		
+		java.awt.Font swingFont = null;
+		boolean registerOkay = true;
+		
+		// Create the internal font from the given file
+		try {
+			swingFont = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, fontFile);
+			if (swingFont == null) registerOkay = false;
+		} catch (Exception e) {
+			registerOkay = false;
+		}
+		
+		// Register the new font with the Swing system
+		if (registerOkay) {
+			registerOkay = GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(swingFont);
+		}
+		
+		// Register the new font with ourselves
+		if (registerOkay) {
+			return register(swingFont).clone();
+		}
+		
+		// If we made it here the registration failed
+		return null;
+	}
+
+	@Override
+	public String[] getRegisteredFontFamilies() {
+		
+		return _fontNames.toArray(new String[_fontNames.size()]);
+	}
+
+	@Override
+	public boolean isFontFamilyRegistered(String fontFamily)
+	{
+		return _fontNames.contains(fontFamily);
+	}
+
+	@Override
+	public Font getActualFont(Font font)
+	{
+		return extractFontForInternalFont(register(font));
+	}
+	
+	/** Gets a string for the font style in the form expected by java.awt.Font.decode(String). */
+	private String getStringForStyle(Font.Style style)
+	{
+		if (style == Font.Style.BOLD_ITALIC) return "BOLDITALIC";
+		else if (style == Font.Style.BOLD) return "BOLD";
+		else if (style == Font.Style.ITALIC) return "ITALIC";
+		else return "PLAIN";
+	}
+	
+	/** Registers the given handle with the manager and returns the internal font. */
+	private java.awt.Font register(Font font)
+	{
+		if (register(null, font) == null) return null;
+		
+		return _fontMap.get(font);
+	}
+
+	/** Registers the given font with the manager and returns the generated handle. */
+	private Font register(java.awt.Font swingFont)
+	{
+		return register(swingFont, null);
+	}
+	
+	/** Registers a handle/font pair. */
+	private Font register(java.awt.Font swingFont, Font font)
+	{
+		if (swingFont == null && font == null) return null;
+		
+		if (_fontMap.containsKey(font)) return font;
+		
+		if (font == null) {
+			font = extractFontForInternalFont(swingFont);
+		} else if (swingFont == null) {
+			swingFont = java.awt.Font.decode(font.getFamilyName() + "-" + getStringForStyle(font.getStyle()) + "-" + font.getSize());
+		}
+		
+		if (swingFont == null || font == null) return null;
+		
+		_fontMap.put(font, swingFont);
+		_fontNames.add(swingFont.getFamily());
+		
+		return font;
+	}
+	
+	/** Creates a handle to match the given Swing font. */
+	private Font extractFontForInternalFont(java.awt.Font swingFont)
+	{
+		if (swingFont == null) return null;
+		
+		Font font = new Font(swingFont.getFamily());
+		font.setStyle(Font.Style.PLAIN);
+		if (swingFont.isBold()) font.toggleBold();
+		if (swingFont.isItalic()) font.toggleItalic();
+		font.setSize(swingFont.getSize());
+		Number spacing = (Number) swingFont.getAttributes().get(java.awt.font.TextAttribute.TRACKING);
+		if (spacing != null) font.setSpacing(spacing.floatValue());
+		
+		return font;
+	}
+	
+	/** Gets the internal Swing font object for use by other Swing managers. */
+	public java.awt.Font getInternalFont(Font font)
+	{
+		return register(font);
+	}
+	
+	/** Registers any standard fonts available on the system with the manager. */
+	private void registerSwingStandardFonts()
+	{
+		String[] standardFontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
+		
+		for (String name : standardFontNames) {
+			register(new Font(name));
+		}
+	}
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingGraphicsManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingGraphicsManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingGraphicsManager.java	(revision 1097)
@@ -0,0 +1,654 @@
+package org.expeditee.gio.swing;
+
+import java.awt.AlphaComposite;
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GraphicsEnvironment;
+import java.awt.RenderingHints;
+import java.awt.Toolkit;
+import java.awt.event.ComponentListener;
+import java.awt.event.KeyListener;
+import java.awt.event.WindowListener;
+import java.awt.event.WindowStateListener;
+import java.awt.geom.AffineTransform;
+import java.awt.image.ImageObserver;
+
+import javax.swing.JFrame;
+import javax.swing.JLayeredPane;
+import javax.swing.JMenuBar;
+import javax.swing.JOptionPane;
+import javax.swing.RepaintManager;
+import javax.swing.SwingUtilities;
+import javax.swing.TransferHandler;
+
+import org.expeditee.core.Clip;
+import org.expeditee.core.Colour;
+import org.expeditee.core.Cursor;
+import org.expeditee.core.Dimension;
+import org.expeditee.core.Fill;
+import org.expeditee.core.Font;
+import org.expeditee.core.GradientFill;
+import org.expeditee.core.Image;
+import org.expeditee.core.Point;
+import org.expeditee.core.Stroke;
+import org.expeditee.core.TextLayout;
+import org.expeditee.core.bounds.PolygonBounds;
+import org.expeditee.gio.GraphicsManager;
+import org.expeditee.gio.GraphicsSurfaceStack;
+import org.expeditee.gio.swing.SwingImageManager.BlockingImageObserver;
+import org.expeditee.gio.swing.SwingImageManager.SelfAnimatingImageObserver;
+import org.expeditee.gui.DisplayController;
+import org.expeditee.items.widgets.Widget;
+import org.expeditee.items.widgets.SwingWidget;
+
+// TODO: Make stack thread-safe
+public class SwingGraphicsManager extends GraphicsManager {
+	
+	private static SwingGraphicsManager _instance;
+	
+	public static SwingGraphicsManager getInstance()
+	{
+		if (_instance == null) {
+			try {
+				SwingUtilities.invokeAndWait(new Runnable() {
+					@Override
+					public void run() {
+						_instance = new SwingGraphicsManager();
+					}
+				});
+			} catch (Exception e) {
+				System.err.println("Error while initialising GraphicsManager. Aborting...");
+				e.printStackTrace(System.err);
+				System.exit(1);
+			}
+			
+			// Block until the window is ready
+			
+		}
+		
+		return _instance;
+	}
+	
+	private JFrame _jFrame;
+	
+	private GraphicsSurfaceStack<Graphics2D> _surfaceStack;
+	
+	private SwingGraphicsManager()
+	{
+		_jFrame = new JFrame() {
+			private static final long serialVersionUID = 5179259234365906415L;
+
+			// Override the paint() method so that Expeditee's graphics are drawn
+			// when the window refreshes.
+			@Override
+			public void paint(Graphics g)
+			{
+				DisplayController.requestRefresh(false);
+			}
+		};
+		
+		setWindowIcon();
+		
+		/*
+		 * See Java bug ID 4016934. They say that window closed events are
+		 * called once the JFrame is disposed.
+		 */
+		_jFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+		
+		// Expeditee handles its own repainting of AWT/Swing components
+		RepaintManager.setCurrentManager(ExpediteeRepaintManager.getInstance());
+		
+		// required to accept TAB key
+		_jFrame.setFocusTraversalKeysEnabled(false);
+		
+		_jFrame.pack();
+		
+		// Moved to here to work with JFXPanel
+		// TODO: Is the above comment still relevant? cts16
+		_jFrame.getContentPane().setLayout(new AbsoluteLayout());
+		
+		// Set visible must be just after DisplayIO.Init for the message box to be the right size
+		// TODO: Is the above comment still relevant? cts16
+		_jFrame.setVisible(true);
+		
+		// Create the surface stack
+		_surfaceStack = new GraphicsSurfaceStack<Graphics2D>() {
+			@Override
+			public Graphics2D getSurfaceFromImage(Image image)
+			{
+				return SwingMiscManager.getIfUsingSwingImageManager().getImageGraphics(image);
+			}
+			
+			@Override
+			public void setSurfaceClip(Graphics2D surface, Clip clip)
+			{
+				if (surface == null) return;
+				
+				if (clip == null) {
+					surface.setClip(null);
+					return;
+				}
+				
+				if (clip.isFullyClipped()) return;
+				
+				surface.setClip(SwingConversions.toSwingRectangle(clip.getBounds()));
+			}
+		};
+		refreshRootSurface();
+	}
+	
+	@Override
+	protected GraphicsSurfaceStack<?> getGraphicsSurfaceStack()
+	{
+		return _surfaceStack;
+	}
+	
+	public Dimension getScreenSize()
+	{
+		java.awt.Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
+		
+		return SwingConversions.fromSwingDimension(size);
+	}
+	
+	public Point getWindowLocation()
+	{
+		return SwingConversions.fromSwingPoint(_jFrame.getContentPane().getLocationOnScreen());
+	}
+	
+	public void setWindowLocation(Point p)
+	{
+		_jFrame.setLocation(p.x, p.y);
+	}
+	
+	public Dimension getWindowSize()
+	{
+		return SwingConversions.fromSwingDimension(_jFrame.getContentPane().getSize());
+	}
+	
+	public void setWindowSize(Dimension d)
+	{
+		_jFrame.setSize(d.width, d.height);
+		_jFrame.setPreferredSize(SwingConversions.toSwingDimension(d));
+	}
+	
+	public void requestFocus()
+	{
+		_jFrame.requestFocus();
+	}
+	
+	public void setCompositeAlpha(float alpha)
+	{
+		_surfaceStack.getCurrentSurface().setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
+	}
+	
+	public void setAntialiasing(boolean on)
+	{
+		setAntialiasing(_surfaceStack.getCurrentSurface(), on);
+	}
+	
+	private void setAntialiasing(Graphics2D surface, boolean on)
+	{
+		if (surface == null) return;
+		
+		if (on) {
+			surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+		} else {
+			surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
+		}
+	}
+	
+	public void setFont(Font font)
+	{
+		_surfaceStack.getCurrentSurface().setFont(SwingMiscManager.getIfUsingSwingFontManager().getInternalFont(font));
+	}
+	
+	public void drawImage(Image image, Point topLeft, Dimension size, double angle, Point cropTopLeft, Dimension cropSize)
+	{
+		// Can't draw nothing
+		if (image == null) return;
+		
+		// Can't draw nowhere
+		if (topLeft == null) return;
+		
+		// If the cropped area is degenerate, abort
+		if (cropSize != null && (cropSize.width <= 0 || cropSize.height <= 0)) return;
+		
+		// If the crop area is the entire image, pretend we are not cropping
+		if (Point.ORIGIN.equals(cropTopLeft) && image.getSize().equals(cropSize)) {
+			cropTopLeft = null;
+			cropSize = null;
+		}
+		
+		SwingImageManager manager = SwingMiscManager.getIfUsingSwingImageManager();
+		
+		ImageObserver animator = null;
+		if (image.isAnimated()) {
+			animator = manager.getAnimator(image);
+			if (animator != null) ((SelfAnimatingImageObserver)animator).activate();
+		} else {
+			animator = new SwingImageManager.LoadCompletedImageObserver();
+		}
+		
+		// Default is the entire image
+		Image croppedImage = image;
+		
+		// If we are cropping, do this now into a temporary image
+		if (cropTopLeft != null && cropSize != null) {
+			croppedImage = manager.createImage(cropSize.width, cropSize.height);
+			manager.getInternalImage(croppedImage).getGraphics().drawImage(manager.getInternalImage(image), 0, 0, cropSize.width - 1, cropSize.height - 1, cropTopLeft.x, cropTopLeft.y, cropTopLeft.x + cropSize.width - 1, cropTopLeft.y + cropSize.height - 1, animator);
+			animator = new BlockingImageObserver();
+		}
+		
+		// Transform the image
+		AffineTransform tx = new AffineTransform();
+		if (size != null) tx.scale(((double) size.width) / croppedImage.getWidth(), ((double) size.height) / croppedImage.getHeight());
+		if (angle != 0.0) tx.rotate(angle);
+		if (topLeft != null) tx.translate(topLeft.x, topLeft.y);
+		
+		// Draw the image to the current surface
+		boolean drawn = _surfaceStack.getCurrentSurface().drawImage(manager.getInternalImage(croppedImage), tx, animator);
+		
+		// If the draw didn't succeed, try again after waiting for the image to load
+		if (!drawn && animator instanceof BlockingImageObserver) {
+			((BlockingImageObserver) animator).attemptWait();
+			_surfaceStack.getCurrentSurface().drawImage(manager.getInternalImage(croppedImage), tx, animator);
+		}
+	}
+	
+	public void drawRectangle(Point topLeft, Dimension size, double angle, Fill fill, Colour borderColour, Stroke borderStroke, Dimension cornerRadius)
+	{
+		Graphics2D g = (Graphics2D) _surfaceStack.getCurrentSurface().create();
+		
+		if (angle != 0.0) {
+			AffineTransform tx = new AffineTransform();
+			tx.rotate(angle);
+			g.transform(tx);
+		}
+		
+		if (fill != null) {
+			if (fill instanceof GradientFill) {
+				g.setPaint(SwingConversions.toSwingGradientPaint((GradientFill) fill));
+			} else {
+				g.setColor(SwingConversions.toSwingColor(fill.getColour()));
+			}
+			
+			if (cornerRadius != null) {
+				g.fillRoundRect(topLeft.x, topLeft.y, size.width, size.height, cornerRadius.width, cornerRadius.height);
+			} else {
+				g.fillRect(topLeft.x, topLeft.y, size.width, size.height);
+			}
+		}
+		
+		if (borderColour != null && borderStroke != null) {
+			g.setColor(SwingConversions.toSwingColor(borderColour));
+			g.setStroke(SwingConversions.toSwingStroke(borderStroke));
+
+			if (cornerRadius != null) {
+				g.drawRoundRect(topLeft.x, topLeft.y, size.width, size.height, cornerRadius.width, cornerRadius.height);
+			} else {
+				g.drawRect(topLeft.x, topLeft.y, size.width, size.height);
+			}
+		}
+		
+		g.dispose();
+	}
+	
+	public void drawOval(Point centre, Dimension diameters, double angle, Fill fill, Colour borderColour, Stroke borderStroke)
+	{
+		Graphics2D g = (Graphics2D) _surfaceStack.getCurrentSurface().create();
+		
+		if (angle != 0.0) {
+			AffineTransform tx = new AffineTransform();
+			tx.rotate(angle);
+			g.transform(tx);
+		}
+		
+		Point topLeft = new Point(centre.x - diameters.width / 2, centre.y - diameters.height / 2);
+		
+		if (fill != null) {
+			if (fill instanceof GradientFill) {
+				g.setPaint(SwingConversions.toSwingGradientPaint((GradientFill) fill));
+			} else {
+				g.setColor(SwingConversions.toSwingColor(fill.getColour()));
+			}
+			
+			g.fillOval(topLeft.x, topLeft.y, diameters.width, diameters.height);
+		}
+		
+		if (borderColour != null && borderStroke != null) {
+			g.setColor(SwingConversions.toSwingColor(borderColour));
+			g.setStroke(SwingConversions.toSwingStroke(borderStroke));
+
+			g.drawOval(topLeft.x, topLeft.y, diameters.width, diameters.height);
+		}
+		
+		g.dispose();
+	}
+	
+	public void drawPolygon(PolygonBounds points, Point offset, Dimension scale, double angle, Fill fill, Colour borderColour, Stroke borderStroke)
+	{
+		if (points == null || points.getPointCount() < 2) return;
+		if (fill == null && (borderColour == null || borderStroke == null)) return;
+		
+		Graphics2D g = (Graphics2D) _surfaceStack.getCurrentSurface().create();
+		
+		AffineTransform tx = new AffineTransform();
+		Point centre = points.getCentre();
+		tx.translate(-centre.x, -centre.y);
+		if (angle != 0.0) tx.rotate(angle);
+		if (scale != null) {
+			double xScale = ((double) scale.width) / (points.getMaxX() - points.getMinX());
+			double yScale = ((double) scale.height) / (points.getMaxY() - points.getMinY());
+			tx.scale(xScale, yScale);
+		}
+		tx.translate(centre.x, centre.y);
+		if (offset != null) tx.translate(offset.x, offset.y);
+		g.transform(tx);
+		
+		if (fill != null) {
+			if (fill instanceof GradientFill) {
+				g.setPaint(SwingConversions.toSwingGradientPaint((GradientFill) fill));
+			} else {
+				g.setColor(SwingConversions.toSwingColor(fill.getColour()));
+			}
+			
+			g.fillPolygon(SwingConversions.toSwingPolygon(points));
+		}
+		
+		if (borderColour != null && borderStroke != null) {
+			g.setColor(SwingConversions.toSwingColor(borderColour));
+			g.setStroke(SwingConversions.toSwingStroke(borderStroke));
+
+			if (points.isClosed()) {
+				g.drawPolygon(SwingConversions.toSwingPolygon(points));
+			} else {
+				int nPoints = points.getPointCount();
+				int[] xPoints = new int[nPoints];
+				int[] yPoints = new int[nPoints];
+				
+				for (int i = 0; i < nPoints; i++) {
+					Point point = points.getPoint(i);
+					xPoints[i] = point.x;
+					yPoints[i] = point.y;
+				}
+				
+				g.drawPolyline(xPoints, yPoints, nPoints);
+			}
+		}
+		
+		g.dispose();
+	}
+	
+	@Override
+	public void drawLine(int x1, int y1, int x2, int y2, Colour colour, Stroke stroke)
+	{
+		if (colour == null || stroke == null) return;
+		
+		Graphics2D g = (Graphics2D) _surfaceStack.getCurrentSurface().create();
+		g.setColor(SwingConversions.toSwingColor(colour));
+		g.setStroke(SwingConversions.toSwingStroke(stroke));
+		
+		g.drawLine(x1, y1, x2, y2);
+		
+		g.dispose();
+	}
+
+	@Override
+	public void drawString(String string, Point position, Font font, Colour colour)
+	{
+		if (string == null || position == null || font == null || colour == null) return;
+		
+		Graphics2D g = (Graphics2D) _surfaceStack.getCurrentSurface().create();
+		g.setColor(SwingConversions.toSwingColor(colour));
+		g.setFont(SwingMiscManager.getIfUsingSwingFontManager().getInternalFont(font));
+		
+		g.drawString(string, position.x, position.y);
+		
+		g.dispose();
+	}
+	
+	@Override
+	public void drawTextLayout(TextLayout layout, Point position, Colour colour)
+	{
+		if (layout == null) return;
+		
+		SwingTextLayoutManager layoutManager = SwingMiscManager.getIfUsingSwingTextLayoutManager();
+		if (layoutManager == null) {
+			drawString(layout.getLine(), position, layout.getFont(), colour);
+			return;
+		}
+		
+		java.awt.font.TextLayout swingLayout = layoutManager.getInternalLayout(layout);
+		
+		Graphics2D g = (Graphics2D) _surfaceStack.getCurrentSurface().create();
+		g.setColor(SwingConversions.toSwingColor(colour));
+		
+		swingLayout.draw(g, position.x, position.y);
+	}
+
+	private Dimension _preFullscreenSize = null;
+	private boolean _fullScreenTransitionPending = false;
+	
+	/**
+	 * Whether or not we are in the middle of transitioning to/from fullscreen.
+	 */
+	public boolean isFullscreenTransitionPending()
+	{
+		return _fullScreenTransitionPending;
+	}
+	
+	/**
+	 * Should be called when the SwingInputManager is notified that the fullscreen transition
+	 * has finished.
+	 */
+	public void finishFullscreenTransition()
+	{
+		_fullScreenTransitionPending = false;
+	}
+	
+	@Override
+	public boolean canGoFullscreen() {
+		return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().isFullScreenSupported();
+	}
+
+	@Override
+	public boolean isFullscreen() {
+		return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getFullScreenWindow() == _jFrame;
+	}
+
+	@Override
+	public void goFullscreen() {
+		if (!canGoFullscreen()) {
+			System.err.println("Warning: GraphicsManager::goFullScreen() called when not available -- ignoring");
+			return;
+		}
+		
+		_preFullscreenSize = getWindowSize();
+		
+		_fullScreenTransitionPending = true;
+		
+		_jFrame.dispose();
+		_jFrame.setUndecorated(true);
+		_jFrame.pack();
+		_jFrame.setVisible(true);
+	        
+		GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow(_jFrame);
+	}
+
+	@Override
+	public void exitFullscreen() {
+		_fullScreenTransitionPending = true;
+		
+		_jFrame.dispose();
+		_jFrame.setUndecorated(false);
+		setWindowSize(_preFullscreenSize);
+		
+		GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow(null);
+		_jFrame.pack();
+		_jFrame.setVisible(true);
+		
+	}
+
+	@Override
+	public void setCursor(Cursor cursor)
+	{
+		if (cursor == null) return;
+		
+		java.awt.Cursor swingCursor;
+		if (cursor.getType() == Cursor.CursorType.CUSTOM) {
+			SwingImageManager imageManager = SwingMiscManager.getIfUsingSwingImageManager();
+			
+			if (imageManager == null) return;
+			
+			swingCursor = Toolkit.getDefaultToolkit().createCustomCursor(imageManager.getInternalImage(cursor.getImage()), SwingConversions.toSwingPoint(cursor.getHotspot()), cursor.getName());
+		} else {
+			swingCursor = new java.awt.Cursor(SwingConversions.toSwingCursorType(cursor.getType()));
+		}
+		
+		_jFrame.setCursor(swingCursor);
+	}
+
+	@Override
+	public Dimension getBestCursorSize(Dimension desiredSize) {
+		Toolkit toolkit = Toolkit.getDefaultToolkit();
+		java.awt.Dimension best_cursor_dim = toolkit.getBestCursorSize(desiredSize.width, desiredSize.height);
+		return SwingConversions.fromSwingDimension(best_cursor_dim);
+	}
+
+	@Override
+	public void setWindowTitle(String title)
+	{
+		_jFrame.setTitle(title);
+	}
+
+	@Override
+	public boolean showDialog(String title, String message) {
+		int result = JOptionPane.showConfirmDialog(_jFrame, message, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
+		return result == JOptionPane.OK_OPTION;
+	}
+
+	@Override
+	public Dimension getCurrentDrawingSurfaceSize()
+	{
+		Image currentImage = _surfaceStack.getCurrentImage();
+		
+		if (currentImage != null) {
+			return currentImage.getSize();
+		} else {
+			return getWindowSize();
+		}
+	}
+	
+	public void refreshRootSurface()
+	{
+		Graphics2D rootSurface = (Graphics2D) _jFrame.getContentPane().getGraphics();
+		setAntialiasing(rootSurface, true);
+		rootSurface.setFont(rootSurface.getFont().deriveFont(40f));
+		_surfaceStack.setRootSurface(rootSurface);
+	}
+	
+	public JFrame getJFrame()
+	{
+		return _jFrame;
+	}
+	
+	public Container getContentPane()
+	{
+		return _jFrame.getContentPane();
+	}
+	
+	public JLayeredPane getLayeredPane()
+	{
+		return _jFrame.getLayeredPane();
+	}
+	
+	public void setTransferHandler(TransferHandler newHandler)
+	{
+		_jFrame.setTransferHandler(newHandler);
+	}
+	
+	public void addWindowListener(WindowListener l)
+	{
+		_jFrame.addWindowListener(l);
+	}
+	
+	public void addWindowStateListener(WindowStateListener l)
+	{
+		_jFrame.addWindowStateListener(l);
+	}
+	
+	public void addKeyListener(KeyListener l)
+	{
+		_jFrame.addKeyListener(l);
+	}
+	
+	public void addComponentListener(ComponentListener l)
+	{
+		_jFrame.addComponentListener(l);
+	}
+	
+	public void setGlassPane(Component glassPane)
+	{
+		_jFrame.setGlassPane(glassPane);
+	}
+	
+	public Component getGlassPane()
+	{
+		return _jFrame.getGlassPane();
+	}
+	
+	public JMenuBar getJMenuBar()
+	{
+		return _jFrame.getJMenuBar();
+	}
+	
+	public FontMetrics getFontMetrics(java.awt.Font font)
+	{
+		return _jFrame.getFontMetrics(font);
+	}
+	
+	public java.awt.Font getFont()
+	{
+		return _jFrame.getFont();
+	}
+	
+	public Graphics2D getCurrentSurface()
+	{
+		return _surfaceStack.getCurrentSurface();
+	}
+	
+	@Override
+	public boolean addInteractiveWidget(Widget iw)
+	{
+		if (super.addInteractiveWidget(iw)) {
+			_jFrame.add(((SwingWidget) iw).getComponent());
+			return true;
+		}
+		
+		return false;
+	}
+	
+	@Override
+	public boolean removeInteractiveWidget(Widget iw)
+	{
+		if (super.removeInteractiveWidget(iw)) {
+			_jFrame.remove(((SwingWidget) iw).getComponent());
+			return true;
+		}
+		
+		return false;
+	}
+	
+	@Override
+	protected void setWindowIcon(Image image)
+	{
+		SwingImageManager imageManager = SwingMiscManager.getIfUsingSwingImageManager();
+		
+		if (imageManager != null) {
+			_jFrame.setIconImage(imageManager.getInternalImage(image));
+		}
+	}
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingImageManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingImageManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingImageManager.java	(revision 1097)
@@ -0,0 +1,527 @@
+package org.expeditee.gio.swing;
+
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GraphicsEnvironment;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.image.BufferedImage;
+import java.awt.image.CropImageFilter;
+import java.awt.image.FilteredImageSource;
+import java.awt.image.ImageObserver;
+import java.awt.image.MemoryImageSource;
+import java.awt.image.PixelGrabber;
+import java.awt.image.VolatileImage;
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+
+import javax.imageio.ImageIO;
+
+import org.expeditee.core.Colour;
+import org.expeditee.core.Image;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.gio.ImageManager;
+import org.expeditee.gui.DisplayController;
+
+import com.sun.pdfview.PDFPage;
+
+/**
+ * The Swing version of the image manager. Mappings are maintained through
+ * the Image handle number so that handles can be garbage-collected i.e. this
+ * manager doesn't hold any reference to the handle object itself. Image.finalize()
+ * tells this manager when the handle is being collected so the mapping can be
+ * removed.
+ * 
+ * @author cts16
+ */
+public class SwingImageManager extends ImageManager {
+
+	/** Singleton instance. */
+	private static SwingImageManager _instance;
+	
+	/** Singleton instantiator. */
+	public static SwingImageManager getInstance()
+	{
+		if (_instance == null) _instance  = new SwingImageManager();
+		
+		return _instance;
+	}
+
+	/** Mapping from image handles to actual internal images. */
+	private HashMap<Long, java.awt.Image> _imageMap;
+	
+	/** Mapping from image handles to image animators (for animated images e.g. GIFs). */
+	private HashMap<Long, SelfAnimatingImageObserver> _animatorMap;
+	
+	/** Constructor. */
+	private SwingImageManager()
+	{
+		// Initialise the maps
+		_imageMap = new HashMap<Long, java.awt.Image>();
+		_animatorMap = new HashMap<Long, SelfAnimatingImageObserver>();	
+	}
+	
+	@Override
+	public org.expeditee.core.Image createImage(int width, int height, boolean hintUseAcceleratedMemory)
+	{
+		// Ensure width and height are sane
+		if (width <= 0 || height <= 0) return null;
+		
+		java.awt.Image swingImage;
+		
+		if (hintUseAcceleratedMemory) {
+			GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+			swingImage = ge.getDefaultScreenDevice().getDefaultConfiguration().createCompatibleVolatileImage(width, height);
+		} else {
+			swingImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+		}
+
+		return register(swingImage, false);
+	}
+
+	@Override
+	public org.expeditee.core.Image createImage(int width, int height, int[] pixelData)
+	{
+		// Ensure width and height are sane
+		if (width <= 0 || height <= 0) return null;
+		
+		// Make sure the caller has provided enough pixel data
+		if (pixelData == null || pixelData.length < (width * height)) return null;
+		
+		// Create the Swing image
+		java.awt.Image swingImage;
+		MemoryImageSource memory_image = new MemoryImageSource(width, height, pixelData, 0, width);
+		swingImage = Toolkit.getDefaultToolkit().createImage(memory_image);
+		
+		// Register the image and return its handle
+		return register(swingImage, false);
+	}
+
+	@Override
+	public org.expeditee.core.Image createImage(int width, int height, PDFPage page)
+	{
+		// Ensure width and height are sane
+		if (width <= 0 || height <= 0) return null;
+		
+		// Ensure we have a valid PDFPage
+		if (page == null) return null;
+		
+		// Get the Swing image of the page
+		java.awt.Image swingImage = page.getImage(width, height, new Rectangle(0, 0, (int)page.getBBox().getWidth(), (int)page.getBBox().getHeight()), null, true, true);
+		
+		// Register it and return the handle
+		return register(swingImage, false);
+	}
+	
+	/**
+	 * Registers the given swing image with the Expeditee image system. Not available from the abstract interface.
+	 * 
+	 * @param image
+	 * 			The image to register.
+	 * 
+	 * @return The internal image handle.
+	 */
+	public org.expeditee.core.Image createImage(java.awt.Image image)
+	{
+		return register(image, false);
+	}
+
+	@Override
+	public org.expeditee.core.Image createImageAsCroppedCopy(org.expeditee.core.Image orig, int x, int y, int width, int height)
+	{
+		// Ensure width and height are sane
+		if (width <= 0 || height <= 0) return null;
+		
+		// Make sure the original is a valid image
+		java.awt.Image swingOrig = getInternalImage(orig);
+		if (swingOrig == null) return null;
+		
+		// Make sure the given coordinate are within the bounds of the image
+		AxisAlignedBoxBounds selectedArea = new AxisAlignedBoxBounds(x, y, width, height);
+		if (!orig.getBounds().completelyContains(selectedArea)) return null;
+		
+		// Create a new image that is a cropped version of the original
+		java.awt.Image swingImage = Toolkit.getDefaultToolkit().createImage(
+				new FilteredImageSource(swingOrig.getSource(),
+						new CropImageFilter(x, y, width, height)
+				)
+		);
+		
+		// Register and return the handle
+		return register(swingImage, false);
+	}
+
+	@Override
+	public org.expeditee.core.Image getImage(URL url)
+	{
+		// Make sure url is valid
+		if (url == null) return null;
+		
+		// Get the image from the given URL
+		java.awt.Image swingImage = Toolkit.getDefaultToolkit().createImage(url);
+		if (swingImage == null) return null;
+		
+		// Register and return the handle
+		return register(swingImage, true);
+	}
+
+	@Override
+	public org.expeditee.core.Image getImage(String filename)
+	{
+		// Make sure the filename is valid
+		if (filename == null) return null;
+		
+		// Get the image from the given file
+		java.awt.Image swingImage = Toolkit.getDefaultToolkit().createImage(filename);
+		if (swingImage == null) return null;
+		
+		// Register and return the handle
+		return register(swingImage, true);
+	}
+
+	@Override
+	public org.expeditee.core.Image getImage(HttpURLConnection connection) throws IOException
+	{
+		// Make sure the connection is valid
+		if (connection == null) return null;
+		
+		// Spoofing a widely accepted User Agent, since some sites refuse to serve non-webbrowser clients
+		connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+		// Read the image from the connection
+		BufferedImage swingImage = ImageIO.read(connection.getInputStream());
+		if (swingImage == null) return null;
+		
+		// Register and return the handle
+		return register(swingImage, false);
+	}
+
+	@Override
+	public synchronized void releaseImage(org.expeditee.core.Image image)
+	{
+		// Make sure the handle corresponds to a valid Swing image
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage == null) return;
+		
+		// Release and forget about the Swing image
+		swingImage.flush();
+		_imageMap.remove(image.getHandle());
+		
+		// If this image is animated, stop the animator
+		SelfAnimatingImageObserver obs = getAnimator(image);
+		if (obs != null) {
+			obs.deactivate();
+			_animatorMap.remove(image.getHandle());
+		}
+	}
+	
+	@Override
+	public synchronized boolean isImageValid(Image image)
+	{
+		// Null image is invalid
+		if (image == null) return false;
+		
+		// Image is valid if it's in the map
+		return _imageMap.containsKey(image.getHandle());
+	}
+
+	@Override
+	public int getWidth(org.expeditee.core.Image image)
+	{
+		// Make sure the image is valid
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage == null) return Image.INVALID_SIZE;
+		
+		// Create a blocking observer so we don't return without an answer
+		BlockingImageObserver obs = new BlockingImageObserver(ImageObserver.WIDTH);
+		
+		// Try and get the width now if it's available
+		int width = swingImage.getWidth(obs);
+		
+		// If it's not available yet, block until it is
+		if (width == Image.INVALID_SIZE) {
+			obs.attemptWait();
+			width = obs.width;
+		}
+		
+		return width;
+	}
+
+	@Override
+	public int getHeight(org.expeditee.core.Image image)
+	{
+		// Make sure the image is valid
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage == null) return Image.INVALID_SIZE;
+
+		// Create a blocking observer so we don't return without an answer
+		BlockingImageObserver obs = new BlockingImageObserver(ImageObserver.HEIGHT);
+
+		// Try and get the height now if it's available
+		int height = swingImage.getHeight(obs);
+		
+		// If it's not available yet, block until it is
+		if (height == Image.INVALID_SIZE) {
+			obs.attemptWait();
+			height = obs.height;
+		}
+		
+		return height;
+	}
+
+	@Override
+	public Colour[] getPixels(org.expeditee.core.Image image, int x, int y, int width, int height)
+	{
+		// Make sure we have a valid image
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage == null) return null;
+		
+		// Make sure width and height are reasonable
+		if (width <= 0 || height <= 0) return null;
+		
+		// Make sure the given coordinate are within the bounds of the image
+		AxisAlignedBoxBounds selectedArea = new AxisAlignedBoxBounds(x, y, width, height);
+		if (!image.getBounds().completelyContains(selectedArea)) return null;
+		
+		// Create an pixel grabber to get the pixel values
+		int[] pixels = new int[width * height];
+		PixelGrabber grabber = new PixelGrabber(swingImage, x, y, width, height, pixels, 0, width);
+		
+		// Block until we've grabbed the pixels
+		boolean pixelsGrabbed = false;
+		while(!pixelsGrabbed) {
+			try {
+				grabber.grabPixels(0);
+				pixelsGrabbed = true;
+			} catch (InterruptedException e) {
+			}
+		}
+		
+		// Return the colour of the grabbed pixel
+		Colour[] ret = new Colour[width * height];
+		for (int i = 0; i < (width * height); i++) {
+			ret[i] = Colour.fromARGB32BitPacked(pixels[i]);
+		}
+		return ret;
+	}
+
+	@Override
+	public void setPixel(org.expeditee.core.Image image, int x, int y, Colour c)
+	{
+		// Make sure we have a valid image
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage == null) return;
+		
+		// Make sure the given coordinate is inside the image
+		if (!image.getBounds().contains(x, y)) return;
+		
+		// Draw to the pixel
+		Graphics g = swingImage.getGraphics();
+		g.setColor(SwingConversions.toSwingColor(c));
+		g.fillRect(x, y, 1, 1);
+	}
+
+	@Override
+	public boolean writeImageToDisk(org.expeditee.core.Image image, String format, File file) throws IOException
+	{
+		// Validate parameters
+		if (format == null || file == null) return false;
+		
+		// Make sure we have a valid image
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage == null) return false;
+		
+		// Write the image to disk
+		if (swingImage instanceof BufferedImage) {
+			return ImageIO.write((BufferedImage) swingImage, format, file);
+		} else {
+			assert(swingImage instanceof VolatileImage);
+			return ImageIO.write(((VolatileImage) swingImage).getSnapshot(), format, file);
+		}
+	}
+	
+	/** Gets the graphics context for drawing on this image. */
+	public Graphics2D getImageGraphics(Image image)
+	{
+		// Make sure we have a valid image
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage == null) return null;
+		
+		// Get the graphics context for the image
+		try {
+			if (swingImage instanceof VolatileImage) {
+				return ((VolatileImage) swingImage).createGraphics();
+			} else {
+				assert (swingImage instanceof BufferedImage);
+				return ((BufferedImage) swingImage).createGraphics();
+			}
+		} catch (Exception e) {
+			e.printStackTrace(System.out);
+			return null;
+		}
+	}
+	
+	/** Gets the Swing image associated with the given handle, or null if none is. */
+	public synchronized java.awt.Image getInternalImage(org.expeditee.core.Image image)
+	{
+		// Make sure we have a valid image
+		if (!isImageValid(image)) return null;
+		
+		// Return the internal image
+		return _imageMap.get(image.getHandle());
+	}
+	
+	/** Creates and returns a handle for the given Swing image to use for future reference. */ 
+	private synchronized org.expeditee.core.Image register(java.awt.Image swingImage, boolean fromDisk)
+	{
+		// TODO: Add animators. cts16
+		// TODO: Only add animators for images we know are animated. cts16
+		
+		// Create a new handle for the image
+		org.expeditee.core.Image image = Image.get(fromDisk);
+		
+		// Put the handle and image in the map
+		_imageMap.put(image.getHandle(), swingImage);
+		
+		// Return the handle
+		return image;
+	}
+	
+	/**
+	 * Gets an animator for the given image. Will reuse an old one if it exists,
+	 * or create one if not.
+	 */
+	public synchronized SelfAnimatingImageObserver getAnimator(org.expeditee.core.Image image)
+	{
+		SelfAnimatingImageObserver obs = _animatorMap.get(image.getHandle());
+		if (obs == null) {
+			obs = new SelfAnimatingImageObserver();
+			_animatorMap.put(image.getHandle(), obs);
+		}
+		return _animatorMap.get(image.getHandle());
+	}
+	
+	/** TODO: Comment. cts16 */
+	public java.awt.image.BufferedImage getBufferedImageForImage(org.expeditee.core.Image image)
+	{
+		if (image == null) return null;
+		
+		java.awt.Image swingImage = getInternalImage(image);
+		if (swingImage instanceof VolatileImage) {
+			return ((VolatileImage) swingImage).getSnapshot();
+		} else if (swingImage instanceof sun.awt.image.ToolkitImage) {
+			return ((sun.awt.image.ToolkitImage) swingImage).getBufferedImage();
+		} else {
+			assert(swingImage instanceof BufferedImage);
+			return (BufferedImage) swingImage;
+		}
+	}
+	
+	/**
+	 * Image observer which blocks the calling thread until image is fully loaded.
+	 */
+	public static class BlockingImageObserver implements ImageObserver
+	{
+		private int _waitingOn;
+		private boolean _done = false;
+		
+		public int x, y, width, height;
+		
+		public BlockingImageObserver()
+		{
+			this(ImageObserver.ALLBITS);
+		}
+		
+		public BlockingImageObserver(int waitingOn)
+		{
+			_waitingOn = waitingOn;
+		}
+		
+		@Override
+		public boolean imageUpdate(java.awt.Image img, int infoflags, int x, int y, int width, int height) {
+			
+			if ((infoflags & _waitingOn) == _waitingOn) {
+				this.x = x;
+				this.y = y;
+				this.width = width;
+				this.height = height;
+				synchronized (this) {
+					_done = true;
+					notify();
+				}
+				return true;
+			}
+			
+			return false;
+		}
+		
+		public void attemptWait()
+		{
+			synchronized (this) {
+				while (!_done) {
+					try {
+						wait();
+					} catch (InterruptedException e) {
+						
+					}
+				}
+			}
+		}
+		
+	}
+	
+	/**
+	 * Image observer which refreshes the display to show a new frame from an animated image.
+	 */
+	public static class SelfAnimatingImageObserver implements ImageObserver
+	{
+		private boolean _active;
+		
+		public SelfAnimatingImageObserver()
+		{
+			_active = true;
+		}
+		
+		public void activate()
+		{
+			_active = true;
+		}
+		
+		public void deactivate()
+		{
+			_active = false;
+		}
+
+		@Override
+		public boolean imageUpdate(java.awt.Image img, int infoflags, int x, int y, int width, int height)
+		{
+			if (!_active) return false;
+			
+			DisplayController.requestRefresh(false);
+			
+			return true;
+		}
+		
+	}
+	
+	/**
+	 * Image observer which refreshes the display once an image finishes loading.
+	 */
+	public static class LoadCompletedImageObserver implements ImageObserver
+	{
+		@Override
+		public boolean imageUpdate(java.awt.Image img, int infoflags, int x, int y, int width, int height)
+		{
+			if ((infoflags & ImageObserver.ALLBITS) != 0) {
+				DisplayController.requestRefresh(false);
+				return false;
+			}
+			
+			return true;
+		}
+	}
+	
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingInputManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingInputManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingInputManager.java	(revision 1097)
@@ -0,0 +1,550 @@
+package org.expeditee.gio.swing;
+
+import java.awt.AWTException;
+import java.awt.Color;
+import java.awt.MouseInfo;
+import java.awt.Robot;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.awt.event.WindowStateListener;
+
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.InputManager;
+import org.expeditee.gio.input.InputEvent;
+import org.expeditee.gio.input.InputEvent.InputType;
+import org.expeditee.gio.input.KBMInputEvent;
+import org.expeditee.gui.Popup;
+import org.expeditee.gui.PopupManager;
+import org.expeditee.items.widgets.Widget;
+
+public class SwingInputManager extends InputManager
+implements 	ComponentListener,
+			WindowListener,
+			WindowStateListener,
+			KeyListener,
+			MouseListener,
+			MouseMotionListener,
+			MouseWheelListener
+{
+	/** Singleton instance. */
+	private static SwingInputManager _instance;
+	
+	/** Singleton constructor. */
+	public static SwingInputManager getInstance()
+	{
+		if (_instance == null) {
+			try {
+				SwingUtilities.invokeAndWait(new Runnable() {
+					@Override
+					public void run() {
+						_instance = new SwingInputManager();
+					}
+				});
+			} catch (Exception e) {
+				System.err.println("Error while initialising InputManager. Aborting...");
+				System.exit(1);
+			}
+		}
+		
+		return _instance;
+	}
+	
+	/** TODO: What functionality does this object provide? */
+	private MouseEventRouter _mouseEventRouter;
+	
+	/** For robotically controlling the mouse position. */
+	private Robot _robot;
+	
+	private SwingInputManager()
+	{
+		SwingInputManager swingInputManager = this;
+		
+		// Sign up to receive input as events
+		SwingGraphicsManager graphicsManager = SwingMiscManager.getIfUsingSwingGraphicsManager();
+		if (graphicsManager != null) {
+			graphicsManager.addWindowListener(swingInputManager);
+			graphicsManager.addWindowStateListener(swingInputManager);
+			graphicsManager.getContentPane().addKeyListener(swingInputManager); // TODO: Why do we need...
+			graphicsManager.addKeyListener(swingInputManager); // TODO: ...both of these? cts16
+			graphicsManager.addComponentListener(swingInputManager);
+			
+			// Sign up to receive mouse events, from the SwingGraphicsManager
+			// TODO: Decide if we will keep using MouseEventRouter. cts16
+			_mouseEventRouter = new MouseEventRouter(graphicsManager.getJMenuBar(), graphicsManager.getContentPane());
+			_mouseEventRouter.addExpediteeMouseListener(swingInputManager);
+			_mouseEventRouter.addExpediteeMouseMotionListener(swingInputManager);
+			_mouseEventRouter.addExpediteeMouseWheelListener(swingInputManager);
+			
+			// enable the glasspane-for capturing all mouse events
+			graphicsManager.setGlassPane(_mouseEventRouter);
+			graphicsManager.getGlassPane().setVisible(true);
+			graphicsManager.getContentPane().setBackground(Color.WHITE);
+			graphicsManager.getContentPane().setFocusTraversalKeysEnabled(false);
+		}
+		
+		// Set up the robot (for controlling the mouse)
+		// TODO: What to do if robot throws exception??? cts16
+		try {
+			_robot = new Robot();
+		} catch (AWTException e) {
+			e.printStackTrace();
+		}
+		
+		// Create the timer for firing timeout events
+		Timer.setLogTimers(false);
+		_timer = new Timer(1, new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				swingInputManager.triggerTimeoutEvents();
+			}
+		});
+		_timer.setRepeats(false);
+	}
+	
+	@Override
+	protected Point getRealCursorPosition()
+	{
+		Point mouseScreenPos = SwingConversions.fromSwingPoint(MouseInfo.getPointerInfo().getLocation());
+		return screenToWindowPosition(mouseScreenPos);
+	}
+
+	@Override
+	public void setCursorPosition(Point position)
+	{
+		Point screenPosition = windowToScreenPosition(position);
+		_robot.mouseMove(screenPosition.x, screenPosition.y);
+	}
+
+	@Override
+	public void windowStateChanged(WindowEvent e)
+	{
+		// Does nothing. cts16
+	}
+
+	@Override
+	public void windowOpened(WindowEvent e)
+	{
+		// Does nothing. cts16
+	}
+
+	@Override
+	public void windowClosing(WindowEvent e)
+	{
+		// Does nothing. cts16
+	}
+
+	@Override
+	public void windowClosed(WindowEvent e)
+	{
+		SwingGraphicsManager g = SwingMiscManager.getIfUsingSwingGraphicsManager();
+		
+		if (g == null || !g.isFullscreenTransitionPending()) {
+			distributeWindowEvent(WindowEventType.WINDOW_CLOSED);
+		}
+	}
+
+	@Override
+	public void windowIconified(WindowEvent e)
+	{
+		// Does nothing. cts16
+	}
+
+	@Override
+	public void windowDeiconified(WindowEvent e)
+	{
+		// Does nothing. cts16
+	}
+
+	@Override
+	public void windowActivated(WindowEvent e)
+	{
+		// Does nothing. cts16
+	}
+
+	@Override
+	public void windowDeactivated(WindowEvent e)
+	{
+		// Does nothing. cts16
+	}
+
+	@Override
+	public void componentResized(ComponentEvent e)
+	{
+		SwingGraphicsManager g = SwingMiscManager.getIfUsingSwingGraphicsManager();
+		if (g != null) g.refreshRootSurface();
+		
+		distributeWindowEvent(WindowEventType.WINDOW_RESIZED);
+	}
+
+	@Override
+	public void componentMoved(ComponentEvent e)
+	{
+		// Does nothing. cts16
+		
+	}
+
+	@Override
+	public void componentShown(ComponentEvent e)
+	{
+		SwingGraphicsManager g = SwingMiscManager.getIfUsingSwingGraphicsManager();
+		
+		if (g != null) g.finishFullscreenTransition();
+	}
+
+	@Override
+	public void componentHidden(ComponentEvent e)
+	{
+		// Does nothing. cts16
+	}
+	
+	/** Converts a location given in screen-space to window-space. */
+	private Point screenToWindowPosition(Point screenPosition)
+	{
+		Point windowTopLeft = EcosystemManager.getGraphicsManager().getWindowLocation();
+		
+		return Point.difference(screenPosition, windowTopLeft);
+	}
+
+	/** Converts a location given in window-space to screen-space. */
+	private Point windowToScreenPosition(Point windowPosition)
+	{
+		Point windowTopLeft = EcosystemManager.getGraphicsManager().getWindowLocation();
+		
+		return windowPosition.clone().add(windowTopLeft);
+	}
+
+	@Override
+	public void mouseWheelMoved(MouseWheelEvent e)
+	{
+		// Give widgets first whack at input
+		distributeNativeInput(e);
+		if (e.isConsumed()) return;
+		
+		try {
+			// Create an input event
+			Integer rotation = new Integer(e.getWheelRotation());
+			InputEvent event = new KBMInputEvent(KBMInputEvent.EventType.MOUSE_WHEEL_SCROLL, rotation);
+			
+			// Translate and perform the input
+			distributeInputEvent(event);
+		} catch (Exception exception) {
+			System.err.println(exception.getMessage());
+		}
+	}
+
+	@Override
+	public void mouseDragged(MouseEvent e)
+	{
+		mouseMoved(e);
+	}
+
+	@Override
+	public void mouseMoved(MouseEvent e)
+	{
+		// Update the cursor position
+		updateCursorPosition(e.getX(), e.getY());
+		
+		// Give widgets first whack at input
+		distributeNativeInput(e);
+		if (e.isConsumed()) return;
+		
+		try {
+			// Create an input event
+			Point movedFrom = getCursorPosition();
+			Point movedTo = new Point(e.getX(), e.getY());
+			InputEvent event = new KBMInputEvent(KBMInputEvent.EventType.MOUSE_MOVE, new Line(movedFrom, movedTo));
+			
+			// Translate and perform the input
+			distributeInputEvent(event);
+		} catch (Exception exception) {
+			System.err.println(exception.getMessage());
+		}
+	}
+
+	@Override
+	public void mouseClicked(MouseEvent e)
+	{
+		// Give widgets first whack at input
+		distributeNativeInput(e);
+		if (e.isConsumed()) return;
+		
+		// Do nothing else (handled by mousePressed and mouseReleased)
+	}
+
+	@Override
+	public void mousePressed(MouseEvent e)
+	{
+		mouseAction(e, true);
+	}
+
+	@Override
+	public void mouseReleased(MouseEvent e)
+	{
+		mouseAction(e, false);
+	}
+	
+	/** Handles mouse pressed/released events. */
+	private void mouseAction(MouseEvent e, boolean down)
+	{
+		// Give widgets first whack at input
+		distributeNativeInput(e);
+		if (e.isConsumed()) return;
+		
+		// Work out which button was pressed
+		KBMInputEvent.MouseButton button;
+		switch (e.getButton()) {
+			case MouseEvent.BUTTON1:
+				button = KBMInputEvent.MouseButton.LEFT;
+				break;
+			case MouseEvent.BUTTON2:
+				button = KBMInputEvent.MouseButton.MIDDLE;
+				break;
+			case MouseEvent.BUTTON3:
+				button = KBMInputEvent.MouseButton.RIGHT;
+				break;
+			default:
+				return;
+		}
+		
+		try {
+			// Create an input event
+			KBMInputEvent.EventType type = down ? KBMInputEvent.EventType.MOUSE_BUTTON_DOWN : KBMInputEvent.EventType.MOUSE_BUTTON_UP;
+			InputEvent event = new KBMInputEvent(type, button);
+			
+			// Translate and perform the input
+			distributeInputEvent(event);
+		} catch (Exception exception) {
+			System.err.println(exception.getMessage());
+		}
+	}
+
+	/** Prevents forwarding mouse-entered-window events when coming back from a widget. */
+	private boolean _inWindow = false;
+	
+	@Override
+	public void mouseEntered(MouseEvent e)
+	{
+		if (!_inWindow) {
+			_inWindow = true;
+			distributeWindowEvent(WindowEventType.MOUSE_ENTERED_WINDOW);
+		}
+	}
+
+	@Override
+	public void mouseExited(MouseEvent e)
+	{
+		_inWindow = false;
+		distributeWindowEvent(WindowEventType.MOUSE_EXITED_WINDOW);
+	}
+
+	@Override
+	public void keyTyped(KeyEvent e)
+	{
+		// Give widgets first whack at input
+		distributeNativeInput(e);
+		if (e.isConsumed()) return;
+		
+		try {
+			// Ignore escape character and control characters
+			if (e.getKeyChar() == KeyEvent.VK_ESCAPE || e.isControlDown() || e.isAltDown()) return;
+			
+			// Create an input event
+			Character character = new Character(e.getKeyChar());
+			InputEvent event = new KBMInputEvent(KBMInputEvent.EventType.CHAR_TYPED, character);
+			
+			// Translate and perform the input
+			distributeInputEvent(event);
+		} catch (Exception exception) {
+			System.err.println(exception.getMessage());
+		}
+	}
+
+	@Override
+	public void keyPressed(KeyEvent e)
+	{
+		keyAction(e, true);
+	}
+
+	@Override
+	public void keyReleased(KeyEvent e)
+	{
+		keyAction(e, false);
+	}
+	
+	/** Handles key pressed/released events. */
+	private void keyAction(KeyEvent e, boolean down)
+	{
+		// Give widgets first whack at input
+		distributeNativeInput(e);
+		if (e.isConsumed()) return;
+		
+		KBMInputEvent.Key key = SwingConversions.fromSwingVirtualKey(e.getKeyCode());
+		if (key == null) return;
+		
+		try {
+			// Create an input event
+			KBMInputEvent.EventType type = down ? KBMInputEvent.EventType.KEY_DOWN : KBMInputEvent.EventType.KEY_UP;
+			InputEvent event = new KBMInputEvent(type, key);
+			
+			// Translate and perform the input
+			distributeInputEvent(event);
+		} catch (Exception exception) {
+			System.err.println(exception.getMessage());
+		}
+	}
+	
+	@Override
+	protected final boolean isInputTypeSupported(InputType type)
+	{
+		// Only keyboard/mouse input is currently supported
+		if (type == InputEvent.InputType.KBM) return true;
+		
+		return false;
+	}
+	
+	/** Forwards the given input event to the listener if it is listening for it. */
+	private void forwardNativeInputToListener(Object listener, java.awt.event.InputEvent event)
+	{
+		if (listener == null || event == null) return;
+		
+		if (listener instanceof KeyListener && event instanceof KeyEvent) {
+			
+			KeyListener keyListener = (KeyListener) listener;
+			KeyEvent ke = (KeyEvent) event;
+			
+			switch (event.getID()) {
+				case KeyEvent.KEY_PRESSED:
+					keyListener.keyPressed(ke);
+					return;
+				case KeyEvent.KEY_RELEASED:
+					keyListener.keyReleased(ke);
+					return;
+				case KeyEvent.KEY_TYPED:
+					keyListener.keyTyped(ke);
+					return;
+			}
+			
+		} else if (event instanceof MouseEvent) {
+			
+			MouseEvent me = (MouseEvent) event;
+			
+			if (listener instanceof MouseListener) {
+				MouseListener mouseListener = (MouseListener) listener;
+				switch (event.getID()) {
+					case MouseEvent.MOUSE_CLICKED:
+						mouseListener.mouseClicked(me);
+						return;
+					case MouseEvent.MOUSE_PRESSED:
+						mouseListener.mousePressed(me);
+						return;
+					case MouseEvent.MOUSE_RELEASED:
+						mouseListener.mouseReleased(me);
+						return;
+					case MouseEvent.MOUSE_ENTERED:
+						mouseListener.mouseEntered(me);
+						return;
+					case MouseEvent.MOUSE_EXITED:
+						mouseListener.mouseExited(me);
+						return;
+				}
+			}
+			
+			if (listener instanceof MouseMotionListener) {
+				MouseMotionListener motionListener = (MouseMotionListener) listener;
+				switch (event.getID()) {
+					case MouseEvent.MOUSE_MOVED:
+						motionListener.mouseMoved(me);
+						return;
+					case MouseEvent.MOUSE_DRAGGED:
+						motionListener.mouseDragged(me);
+						return;
+				}
+			}
+			
+			if (listener instanceof MouseWheelListener && me instanceof MouseWheelEvent) {
+				MouseWheelListener wheelListener = (MouseWheelListener) listener;
+				MouseWheelEvent mwe = (MouseWheelEvent) me;
+				switch (event.getID()) {
+					case MouseEvent.MOUSE_WHEEL:
+						wheelListener.mouseWheelMoved(mwe);
+						return;
+				}
+			}
+		}
+	}
+	
+	/** Distributes the native input to all pop-ups and widgets. */
+	private void distributeNativeInput(java.awt.event.InputEvent event)
+	{
+		// Distribute to pop-ups first
+		distributeNativeInputToPopups(event);
+		if (event.isConsumed()) return;
+		
+		// Then distribute to widgets
+		distributeNativeInputToWidgets(event);
+	}
+	
+	/** Distributes the input event to registered widgets. */
+	private void distributeNativeInputToPopups(java.awt.event.InputEvent event)
+	{
+		if (event == null) return;
+		
+		for (Popup popup : PopupManager.getInstance().getPopups()) {
+			forwardNativeInputToListener(popup, event);
+			
+			if (event.isConsumed()) return;
+		}
+	}
+	
+	/** Distributes the input event to registered widgets. */
+	private void distributeNativeInputToWidgets(java.awt.event.InputEvent event)
+	{
+		if (event == null) return;
+		
+		for (Widget iw : _widgets) {
+			java.awt.event.InputEvent e = respecInputEventForWidget(event, iw);
+			
+			forwardNativeInputToListener(iw, e);
+			
+			if (e != event && e.isConsumed()) event.consume();
+			
+			if (event.isConsumed()) return;
+		}
+	}
+	
+	/** Modifies the event so that it is relative to the given widget. */
+	private java.awt.event.InputEvent respecInputEventForWidget(java.awt.event.InputEvent event, Widget iw)
+	{
+		// TODO: Complete. cts16
+		return event;
+	}
+
+	/** The timer which fires timeout events for the Swing input manager. */
+	private Timer _timer;
+	
+	@Override
+	protected void updateTimer(long nextTimeout)
+	{
+		if (nextTimeout < 0) nextTimeout = 0;
+		_timer.stop();
+		_timer.setInitialDelay((int) nextTimeout);
+		_timer.start();
+		
+	}
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingMiscManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingMiscManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingMiscManager.java	(revision 1097)
@@ -0,0 +1,189 @@
+package org.expeditee.gio.swing;
+
+import java.awt.Desktop;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import javax.swing.AbstractButton;
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+import javax.swing.SwingUtilities;
+
+import org.expeditee.Util;
+import org.expeditee.core.Image;
+import org.expeditee.gio.EcosystemManager;
+import org.expeditee.gio.FontManager;
+import org.expeditee.gio.GraphicsManager;
+import org.expeditee.gio.ImageManager;
+import org.expeditee.gio.MiscManager;
+import org.expeditee.gio.TextLayoutManager;
+
+import javafx.embed.swing.SwingFXUtils;
+
+public class SwingMiscManager extends MiscManager {
+	
+	private static SwingMiscManager _instance;
+	
+	public static SwingMiscManager getInstance()
+	{
+		if (_instance == null) _instance = new SwingMiscManager();
+		
+		return _instance;
+	}
+	
+	public static void setJButtonIcon(AbstractButton jbutton, Image image)
+	{
+		if (jbutton == null || image == null) return;
+		
+		SwingImageManager manager = getIfUsingSwingImageManager();
+		
+		if (manager == null) return;
+		
+		Icon icon = new ImageIcon(manager.getInternalImage(image));
+		
+		jbutton.setIcon(icon);
+	}
+	
+	public static void setJButtonSelectedIcon(AbstractButton jbutton, Image image)
+	{
+		if (jbutton == null || image == null) return;
+		
+		SwingImageManager manager = getIfUsingSwingImageManager();
+		
+		if (manager == null) return;
+		
+		Icon icon = new ImageIcon(manager.getInternalImage(image));
+		
+		jbutton.setSelectedIcon(icon);
+	}
+	
+	/**
+	 * Returns the SwingImageManager if it is the ImageManager for this session,
+	 * or null if another ImageManager is being used.
+	 */
+	public static SwingImageManager getIfUsingSwingImageManager()
+	{
+		ImageManager manager = org.expeditee.core.Image.getManager();
+		if (manager instanceof SwingImageManager) {
+			return (SwingImageManager) manager;
+		}
+		
+		return null;
+	}
+	
+	/**
+	 * Returns the SwingFontManager if it is the FontManager for this session,
+	 * or null if another FontManager is being used.
+	 */
+	public static SwingFontManager getIfUsingSwingFontManager()
+	{
+		FontManager manager = EcosystemManager.getFontManager();
+		if (manager instanceof SwingFontManager) {
+			return (SwingFontManager) manager;
+		}
+		
+		return null;
+	}
+	
+	/**
+	 * Returns the SwingGraphicsManager if it is the GraphicsManager for this session,
+	 * or null if another GraphicsManager is being used.
+	 */
+	public static SwingGraphicsManager getIfUsingSwingGraphicsManager()
+	{
+		GraphicsManager manager = EcosystemManager.getGraphicsManager();
+		if (manager instanceof SwingGraphicsManager) {
+			return (SwingGraphicsManager) manager;
+		}
+		
+		return null;
+	}
+	
+	/**
+	 * Returns the SwingTextLayoutManager if it is the TextLayoutManager for this session,
+	 * or null if another TextLayoutManager is being used.
+	 */
+	public static SwingTextLayoutManager getIfUsingSwingTextLayoutManager()
+	{
+		TextLayoutManager manager = EcosystemManager.getTextLayoutManager();
+		if (manager instanceof SwingTextLayoutManager) {
+			return (SwingTextLayoutManager) manager;
+		}
+		
+		return null;
+	}
+	
+	public static org.expeditee.core.Image getImageForJavaFXImage(javafx.scene.image.Image image)
+	{
+		BufferedImage swingImage = SwingFXUtils.fromFXImage(image, null);
+		
+		return getIfUsingSwingImageManager().createImage(swingImage);
+	}
+	
+	@Override
+	public void beep()
+	{
+		java.awt.Toolkit.getDefaultToolkit().beep();
+	}
+	
+	@Override
+	public boolean browse(String url)
+	{
+		try {
+			URL check = new URL(url); // TODO: Why do we need this? cts16	
+			Desktop.getDesktop().browse(new URI(url));
+		} catch (IOException | URISyntaxException e) {
+			return false;
+		}
+		
+		return true;
+	}
+	
+	@Override
+	public String print(String file)
+	{
+		try {
+			if (Util.isMinimumVersion6()) {
+				if (Desktop.isDesktopSupported()) {
+					Desktop.getDesktop().print(new File(file));
+				}
+			}
+		} catch (Exception e) {
+			return e.getMessage();
+		}
+		
+		return null;
+	}
+
+	@Override
+	public String open(String file)
+	{
+		try {
+			if (Util.isMinimumVersion6()) {
+				if (Desktop.isDesktopSupported()) {
+					Desktop.getDesktop().open(new File(file));
+				}
+			}
+		} catch (Exception e) {
+			return e.getStackTrace().toString();
+		}
+		
+		return null;
+	}
+
+	@Override
+	protected void scheduleRunnable(Runnable routine)
+	{
+		SwingUtilities.invokeLater(routine);
+	}
+
+	@Override
+	protected boolean isGIOThread()
+	{
+		return SwingUtilities.isEventDispatchThread();
+	}
+}
Index: /trunk/src/org/expeditee/gio/swing/SwingTextLayoutManager.java
===================================================================
--- /trunk/src/org/expeditee/gio/swing/SwingTextLayoutManager.java	(revision 1097)
+++ /trunk/src/org/expeditee/gio/swing/SwingTextLayoutManager.java	(revision 1097)
@@ -0,0 +1,278 @@
+package org.expeditee.gio.swing;
+
+import java.awt.Rectangle;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.TextAttribute;
+import java.text.AttributedString;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.expeditee.core.Font;
+import org.expeditee.core.Line;
+import org.expeditee.core.Point;
+import org.expeditee.core.TextHitInfo;
+import org.expeditee.core.TextLayout;
+import org.expeditee.core.bounds.AxisAlignedBoxBounds;
+import org.expeditee.gio.TextLayoutManager;
+
+/**
+ * TODO: Comment. cts16
+ * 
+ * @author cts16
+ */
+public class SwingTextLayoutManager extends TextLayoutManager
+{
+	/** Singleton instance. */
+	private static SwingTextLayoutManager _instance = null;
+	
+	/** Singleton instantiator. */
+	public static SwingTextLayoutManager getInstance()
+	{
+		if (_instance == null) _instance = new SwingTextLayoutManager();
+		
+		return _instance;
+	}
+	
+	/** Mapping from Expeditee text-layout objects to swing equivalents. */
+	private HashMap<Long, java.awt.font.TextLayout> _layoutMap;
+	
+	private SwingTextLayoutManager()
+	{
+		_layoutMap = new HashMap<Long, java.awt.font.TextLayout>();
+	}
+	
+	private synchronized void register(TextLayout layout, java.awt.font.TextLayout swingLayout)
+	{
+		if (layout == null || swingLayout == null) return;
+		
+		_layoutMap.put(layout.getHandle(), swingLayout);
+	}
+	
+	public synchronized java.awt.font.TextLayout getInternalLayout(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return _layoutMap.get(layout.getHandle());
+	}
+
+	public synchronized boolean isTextLayoutValid(TextLayout layout)
+	{
+		if (layout == null) return false;
+		
+		return _layoutMap.containsKey(layout.getHandle());
+	}
+
+	@Override
+	public float getAdvance(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return Float.NaN;
+		
+		return getInternalLayout(layout).getAdvance();
+	}
+
+	@Override
+	public int getCharacterCount(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return 0;
+		
+		return getInternalLayout(layout).getCharacterCount();
+	}
+
+	@Override
+	public TextHitInfo getNextLeftHit(TextLayout layout, int offset)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return SwingConversions.fromSwingTextHitInfo(getInternalLayout(layout).getNextLeftHit(offset));
+	}
+
+	@Override
+	public TextHitInfo getNextLeftHit(TextLayout layout, TextHitInfo hit)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return SwingConversions.fromSwingTextHitInfo(getInternalLayout(layout).getNextLeftHit(SwingConversions.toSwingTextHitInfo(hit)));
+	}
+
+	@Override
+	public TextHitInfo getNextRightHit(TextLayout layout, int offset)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return SwingConversions.fromSwingTextHitInfo(getInternalLayout(layout).getNextRightHit(offset));
+	}
+
+	@Override
+	public TextHitInfo getNextRightHit(TextLayout layout, TextHitInfo hit)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return SwingConversions.fromSwingTextHitInfo(getInternalLayout(layout).getNextRightHit(SwingConversions.toSwingTextHitInfo(hit)));
+	}
+
+	@Override
+	public float[] getCaretInfo(TextLayout layout, TextHitInfo hit)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return getInternalLayout(layout).getCaretInfo(SwingConversions.toSwingTextHitInfo(hit));
+	}
+
+	@Override
+	public TextHitInfo hitTestChar(TextLayout layout, float x, float y)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return SwingConversions.fromSwingTextHitInfo(getInternalLayout(layout).hitTestChar(x, y));
+	}
+
+	@Override
+	public AxisAlignedBoxBounds getPixelBounds(TextLayout layout, float x, float y)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		return SwingConversions.fromSwingRectangle(getInternalLayout(layout).getPixelBounds(null, x, y));
+	}
+
+	@Override
+	public AxisAlignedBoxBounds getLogicalHighlightShape(TextLayout layout, int firstEndpoint, int secondEndpoint)
+	{
+		if (!isTextLayoutValid(layout)) return null;
+		
+		Rectangle rect = getInternalLayout(layout).getLogicalHighlightShape(firstEndpoint, secondEndpoint).getBounds();
+		
+		return SwingConversions.fromSwingRectangle(rect);
+	}
+
+	@Override
+	public float getAscent(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return Float.NaN;
+		
+		return getInternalLayout(layout).getAscent();
+	}
+
+	@Override
+	public float getDescent(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return Float.NaN;
+		
+		return getInternalLayout(layout).getDescent();
+	}
+
+	@Override
+	public float getLeading(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return Float.NaN;
+		
+		return getInternalLayout(layout).getLeading();
+	}
+
+	@Override
+	public synchronized void releaseLayout(TextLayout layout)
+	{
+		if (!isTextLayoutValid(layout)) return;
+		
+		_layoutMap.remove(layout.getHandle());
+	}
+	
+	@Override
+	public List<TextLayout> layoutString(String string, Font font, Point start, Line[] lineBreakers, int widthLimit, int lineSpacing, boolean dontBreakWords, boolean fullJustify)
+	{
+		if (string == null || font == null || start == null) return null;
+		
+		// Make sure we have a swing font to use
+		java.awt.Font swingFont;
+		SwingFontManager fontManager = SwingMiscManager.getIfUsingSwingFontManager();
+		if (fontManager == null) return null;
+		swingFont = fontManager.getInternalFont(font);
+		if (swingFont == null) return null;
+		
+		// Temporary list to accumulate the TextLayouts in to
+		List<java.awt.font.TextLayout> layouts = new LinkedList<java.awt.font.TextLayout>();
+		List<Integer> offsets = new LinkedList<Integer>();
+		offsets.add(0);
+		List<Point> positions = new LinkedList<Point>();
+		positions.add(new Point(start));
+		
+		// Set up a line-break measurer to layout the text
+		AttributedString paragraphText = new AttributedString(string);
+		paragraphText.addAttribute(TextAttribute.FONT, swingFont);
+		FontRenderContext frc = new FontRenderContext(null, true, true);
+		LineBreakMeasurer lineBreaker = new LineBreakMeasurer(paragraphText.getIterator(), frc);
+		lineBreaker.setPosition(0);
+		
+		// Keep laying out text until the input string is completely laid out
+		while (lineBreaker.getPosition() < string.length()) {
+			
+			int width = widthLimit;
+			if (lineBreakers != null) width = Math.min(width, (int) getLineWidth(start, lineBreakers));
+			
+			int end = string.length();
+			for (int i = lineBreaker.getPosition() + 1; i < string.length(); i++) {
+				if (string.charAt(i) == '\n') {
+					end = i;
+					break;
+				}
+			}
+			
+			java.awt.font.TextLayout layout = lineBreaker.nextLayout(width, end, dontBreakWords);
+			
+			// If it's impossible to layout any more text without breaking a word, just do it
+			if (layout == null && width == widthLimit) {
+				layout = lineBreaker.nextLayout(width, end, false);
+				// If still impossible, give up
+				if (layout == null) break;
+			}
+
+			if (fullJustify && lineBreaker.getPosition() < string.length()) {
+				layout = layout.getJustifiedLayout(width);
+			}
+			
+			layouts.add(layout);
+			offsets.add(lineBreaker.getPosition());
+			int lineDrop = (int) (layout.getAscent() + layout.getDescent());
+			if (lineSpacing >= 0) {
+				lineDrop += lineSpacing; 
+			} else {
+				lineDrop += layout.getLeading();
+			}
+			start.y += lineDrop;
+			positions.add(new Point(start));
+		}
+		
+		// Convert the accumulated list into an internal-type array
+		List<TextLayout> ret = new LinkedList<TextLayout>();
+		for (int i = 0; i < layouts.size(); i++) {
+			java.awt.font.TextLayout swingLayout = layouts.get(i);
+			int startCharIndex = offsets.get(i);
+			int endCharIndex = offsets.get(i + 1) - 1;
+			String line = string.substring(startCharIndex, endCharIndex + 1);
+			TextLayout layout = TextLayout.get(line, font, startCharIndex, endCharIndex);
+			register(layout, swingLayout);
+			ret.add(layout);
+		}
+		
+		return ret;
+		
+	}
+
+	@Override
+	public int getStringWidth(Font font, String string)
+	{
+		SwingGraphicsManager g = SwingMiscManager.getIfUsingSwingGraphicsManager();
+		SwingFontManager f = SwingMiscManager.getIfUsingSwingFontManager();
+		
+		if (g == null || f == null) return -1;
+		
+		if (font != null) {
+			return g.getFontMetrics(f.getInternalFont(font)).stringWidth(string);
+		} else {
+			return g.getFontMetrics(f.getInternalFont(f.getDefaultFont())).stringWidth(string);
+		}
+		
+	}
+
+}
