@SuppressWarnings("serial")
public class ColourChooser extends WebDialog implements ColourEventBroadcaster
{
	private static final int MODE_COUNT = 4;
	
	private int[] primary = {0, 0, 0, 0, 0, 0, 255};
	private int[] secondary = {0, 0, 0, 0, 0, 0, 255};
	private int mode;
	
	private List<ColourListener> listeners;
	
	private PalettePanel palletPanel;
	
	private JComponent wheel, rgbPanel, hsvPanel, alphaPanel, indicator, modePanel, settings;
	private JPanel top, sliders, left;
	
	public ColourChooser()
	{
		super();
		this.setTitle("Colour Chooser");
		this.setIconImage(GUIManager.ICON);
		setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
		
		listeners = new ArrayList<>();
		
		palletPanel = new PalettePanel(this, Palette.defaultPallet());
		wheel = new ColorWheel(this);
		HexColourField hexField = new HexColourField(this);
		indicator = new ColorIndicator(this, hexField);
		modePanel = new ModePanel(this);
		
		rgbPanel = new JPanel();
		hsvPanel = new JPanel();
		alphaPanel = new JPanel();
		rgbPanel.setLayout(new BoxLayout(rgbPanel, BoxLayout.X_AXIS));
		hsvPanel.setLayout(new BoxLayout(hsvPanel, BoxLayout.X_AXIS));
		alphaPanel.setLayout(new BoxLayout(alphaPanel, BoxLayout.X_AXIS));
		
		JPanel sliderPanel = new JPanel();
		JPanel fieldPanel = new JPanel();
		sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.Y_AXIS));
		fieldPanel.setLayout(new BoxLayout(fieldPanel, BoxLayout.Y_AXIS));
		
		fieldPanel.add(new Spacer(0, 14));
		fieldPanel.add(new ColourSliderField(Channel.Red, this));
		fieldPanel.add(new ColourSliderField(Channel.Green, this));
		fieldPanel.add(new ColourSliderField(Channel.Blue, this));
		sliderPanel.add(new SpacerLabel("<html><b>RGB:</b></html>", 48, 16));
		sliderPanel.add(new ColourSlider(Channel.Red, this));
		sliderPanel.add(new Spacer(0, 8));
		sliderPanel.add(new ColourSlider(Channel.Green, this));
		sliderPanel.add(new Spacer(0, 8));
		sliderPanel.add(new ColourSlider(Channel.Blue, this));
		
		rgbPanel.add(sliderPanel);
		rgbPanel.add(fieldPanel);
		
		sliderPanel = new JPanel();
		fieldPanel = new JPanel();
		sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.Y_AXIS));
		fieldPanel.setLayout(new BoxLayout(fieldPanel, BoxLayout.Y_AXIS));
		
		fieldPanel.add(new Spacer(0, 14));
		fieldPanel.add(new ColourSliderField(Channel.Hue, this));
		fieldPanel.add(new ColourSliderField(Channel.Saturation, this));
		fieldPanel.add(new ColourSliderField(Channel.Value, this));
		sliderPanel.add(new SpacerLabel("<html><b>HSV:</b></html>", 48, 16));
		sliderPanel.add(new ColourSlider(Channel.Hue, this));
		sliderPanel.add(new Spacer(0, 8));
		sliderPanel.add(new ColourSlider(Channel.Saturation, this));
		sliderPanel.add(new Spacer(0, 8));
		sliderPanel.add(new ColourSlider(Channel.Value, this));
		
		hsvPanel.add(sliderPanel);
		hsvPanel.add(fieldPanel);
		
		sliderPanel = new JPanel();
		fieldPanel = new JPanel();
		sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.Y_AXIS));
		fieldPanel.setLayout(new BoxLayout(fieldPanel, BoxLayout.Y_AXIS));
		
		fieldPanel.add(new Spacer(0, 14));
		fieldPanel.add(new ColourSliderField(Channel.Alpha, this));
		sliderPanel.add(new SpacerLabel("<html><b>Alpha:</b></html>", 48, 16));
		sliderPanel.add(new ColourSlider(Channel.Alpha, this));
		
		alphaPanel.add(sliderPanel);
		alphaPanel.add(fieldPanel);
		
		settings = new JButton("Settings");
		
		left = new JPanel();
		left.setLayout(new BorderLayout());
		left.add(indicator, BorderLayout.NORTH);
		left.add(settings, BorderLayout.CENTER);
		left.add(modePanel, BorderLayout.SOUTH);
		
		top = new JPanel();
		top.setLayout(new BoxLayout(top, BoxLayout.X_AXIS));
		top.add(left);
		top.add(wheel);
		
		JPanel bottom = new JPanel();
		bottom.setLayout(new BoxLayout(bottom, BoxLayout.Y_AXIS));
		bottom.add(top);
		bottom.add(palletPanel);
		
		sliders = new JPanel();
		sliders.setLayout(new BoxLayout(sliders, BoxLayout.Y_AXIS));
		sliders.add(rgbPanel);
		sliders.add(hsvPanel);
		sliders.add(alphaPanel);
		
		JPanel all = new JPanel();
		
		all.add(bottom);
		all.add(sliders);
		
		add(all);
		
		indicator.setAlignmentX(LEFT_ALIGNMENT);
		indicator.setAlignmentY(TOP_ALIGNMENT);
		
		broadcastChanges(null);
		
		this.mode = 2;
		this.switchVisible();
		
		pack();
		setAlwaysOnTop(true);
		setLocationRelativeTo(null);
		this.setResizable(false);
		this.setVisible(false);
	}
	
	public void toggle()
	{
		this.setVisible(!this.isVisible());
	}
	
	@Override
	public void addColorListener(ColourListener c)
	{
		listeners.add(c);
	}
	
	@Override
	public void removeColorListener(ColourListener c)
	{
		listeners.remove(c);
	}
	
	@Override
	public void broadcastChanges(ColourListener source)
	{
		for(ColourListener l : listeners)
		{
			if(l.equals(source))
				continue;
			l.changeColor(primary[0], primary[1], primary[2], primary[3], primary[4], primary[5], primary[6], true);
			l.changeColor(secondary[0], secondary[1], secondary[2], secondary[3], secondary[4], secondary[5], secondary[6], false);
		}
		
		getContentPane().repaint();
	}
	
	public int red(boolean primary)
	{
		return (primary ? this.primary : this.secondary)[Channel.Red.ordinal()];
	}
	
	public int green(boolean primary)
	{
		return (primary ? this.primary : this.secondary)[Channel.Green.ordinal()];
	}
	
	public int blue(boolean primary)
	{
		return (primary ? this.primary : this.secondary)[Channel.Blue.ordinal()];
	}
	
	public int hue(boolean primary)
	{
		return (primary ? this.primary : this.secondary)[Channel.Hue.ordinal()];
	}
	
	public int sat(boolean primary)
	{
		return (primary ? this.primary : this.secondary)[Channel.Saturation.ordinal()];
	}
	
	public int val(boolean primary)
	{
		return (primary ? this.primary : this.secondary)[Channel.Value.ordinal()];
	}
	
	public boolean showMore()
	{
		if(this.mode < MODE_COUNT)
		{
			mode++;
			switchVisible();
		}
		return mode != MODE_COUNT;
	}
	
	public boolean showLess()
	{
		if(this.mode > 0)
		{
			mode--;
			switchVisible();
		}
		return mode != 0;
	}
	
	public void setMode(int mode)
	{
		this.mode = MathUtils.clamp(mode, 0, MODE_COUNT);
		switchVisible();
	}
	
	public int getMode()
	{
		return this.mode;
	}
	
	public void switchVisible()
	{
		wheel.setVisible(this.mode > 1);
		rgbPanel.setVisible(this.mode > 0);
		hsvPanel.setVisible(this.mode > 1);
		alphaPanel.setVisible(this.mode > 3);
		settings.setVisible(this.mode > 1);
		
		if(this.mode > 3)
		{
			palletPanel.setMode(PaletteMode.Extended);
		}
		else if(this.mode > 1)
		{
			palletPanel.setMode(PaletteMode.Standard);
		}
		else
		{
			palletPanel.setMode(PaletteMode.Minimized);
		}
		
		if(this.mode != 1)
		{
			sliders.add(rgbPanel, 0);
			if(top.getComponent(top.getComponentCount() - 1) instanceof Spacer)
				top.remove(top.getComponentCount() - 1);
		}
		else
		{
			top.add(new Spacer(8, 0));
			top.add(rgbPanel);
		}
		
		if(this.mode > 0)
		{
			left.add(indicator, BorderLayout.NORTH);
			left.add(settings, BorderLayout.CENTER);
			left.add(modePanel, BorderLayout.SOUTH);
		}
		else
		{
			left.add(indicator, BorderLayout.WEST);
			left.add(settings, BorderLayout.CENTER);
			left.add(modePanel, BorderLayout.EAST);
		}
		
		rgbPanel.getComponent(1).setVisible(this.mode > 2);
		hsvPanel.getComponent(1).setVisible(this.mode > 2);
		alphaPanel.getComponent(1).setVisible(this.mode > 2);
		
		//this.revalidate();
		this.pack();
		this.setResizable(false);
	}
	
	@Override
	public void makeChange(Object source, Channel channel, int val, boolean selected)
	{
		int[] colour = selected ? this.primary : this.secondary;
		
		colour[channel.ordinal()] = val;
		
		int mode = channel.ordinal() / 3;
		
		if(mode == 0)
		{
			int r = red(selected);
			int g = green(selected);
			int b = blue(selected);
			if(!(r + g + b == 0))
			{
				long hsva = toHSVA(r / 255., g / 255., b / 255., 1);
				colour[Channel.Hue.ordinal()] = (int) ((hsva >> 32) & 0x3FF);
				colour[Channel.Saturation.ordinal()] = (int) ((hsva >> 16) & 0xFF);
				colour[Channel.Value.ordinal()] = (int) ((hsva >> 8) & 0xFF);
			}
			else
			{
				colour[Channel.Value.ordinal()] = 0;
			}
		}
		else if(mode == 1)
		{
			int argb = toARGB(hue(selected) / 1023., sat(selected) / 255., val(selected) / 255., 1);
			colour[Channel.Red.ordinal()] = (argb >> 16) & 0xFF;
			colour[Channel.Green.ordinal()] = (argb >> 8) & 0xFF;
			colour[Channel.Blue.ordinal()] = (argb >> 0) & 0xFF;
		}
		
		if(source == listeners.get(3 + Channel.Hue.ordinal()) && sat(selected) == 0)
		{
			
		}
	}
	
	public void setColour(int colour, Object source, boolean primary)
	{
		this.makeChange(source, Channel.Blue, (colour) & 0xFF, primary);
		this.makeChange(source, Channel.Green, (colour >>= 8) & 0xFF, primary);
		this.makeChange(source, Channel.Red, (colour >>= 8) & 0xFF, primary);
		this.makeChange(source, Channel.Alpha, (colour >>= 8) & 0xFF, primary);
		this.broadcastChanges(null);
	}
	
	/*
	public static void main(String[] a) throws IOException
	{
		GUIManager.setLAF();
		new ColourChooser();
	}
	*/
} public interface ColourListener extends EventListener
{
	public void changeColor(int r, int g, int b, int h, int s, int v, int a, boolean primary);
} @SuppressWarnings("serial")
public class AboutDialog extends WebDialog
{
	public AboutDialog(JFrame frame)
	{
		super(frame, "About Spade");
		this.setIconImage(GUIManager.ICON);
		this.center(400, 300);
		this.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
		
		WebTextPane area = new WebTextPane();
		StyledDocument doc = area.getStyledDocument();
		
		addStylesToDocument(doc);
		
		append(doc, "Spade", "large");
		append(doc, "\n    Version: ", "bold");
		append(doc, Spade.getVersion().toString(), "regular");
		append(doc, "\n    Build Type: ", "bold");
		append(doc, Spade.BUILD_TYPE, "regular");
		append(doc, "\n    Released on: ", "bold");
		append(doc, Spade.RELEASED, "regular");
		append(doc, "\n\n", "regular");
		append(doc, "Spade is an open-source cross-platform raster graphics editor inspired by Paint.NET.", "regular");
		append(doc, "It was started, and is currently maintained by HeroesGrave.\n", "regular");
		append(doc, "\n", "regular");
		append(doc, "Links:\n", "large");
		append(doc, "    Github Repo:\n", "bold");
		append(doc, "        " + Spade.REPO_URL + "\n", "italic");
		append(doc, "    HeroesGrave Development:\n", "bold");
		append(doc, "        http://heroesgrave.github.io/\n", "italic");
		append(doc, "\n", "regular");
		append(doc, "Credits:\n", "large");
		append(doc, "    HeroesGrave\n", "regular");
		append(doc, "    Longor1996\n", "regular");
		append(doc, "    BurntPizza\n", "regular");
		append(doc, "    SuperDisk\n", "regular");
		append(doc, "    Gef4k\n", "regular");
		append(doc, "    MarkBernard\n", "regular");
		append(doc, "    pwnedary\n", "regular");
		
		area.setEditable(false);
		
		this.getContentPane().setLayout(new BorderLayout());
		this.getContentPane().add(new JScrollPane(area), BorderLayout.CENTER);
		this.setResizable(false);
	}
	
	public static void append(StyledDocument doc, String str, String sty)
	{
		try
		{
			doc.insertString(doc.getLength(), str, doc.getStyle(sty));
		}
		catch(BadLocationException e)
		{
			e.printStackTrace();
		}
	}
	
	public static void addStylesToDocument(StyledDocument doc)
	{
		//Initialize some styles.
		Style def = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
		
		Style regular = doc.addStyle("regular", def);
		StyleConstants.setFontFamily(def, "SansSerif");
		
		Style s = doc.addStyle("italic", regular);
		StyleConstants.setItalic(s, true);
		
		s = doc.addStyle("bold", regular);
		StyleConstants.setBold(s, true);
		
		s = doc.addStyle("small", regular);
		StyleConstants.setFontSize(s, 10);
		
		s = doc.addStyle("large", regular);
		StyleConstants.setFontSize(s, 16);
		StyleConstants.setBold(s, true);
	}
} public class LayerManager implements CheckStateChangeListener<Layer>, TreeSelectionListener
{
	public WebDialog dialog;
	
	protected LayerSettings lsettings;
	
	protected Layer rootNode;
	protected DefaultTreeModel model;
	protected WebCheckBoxTree<Layer> tree;
	protected WebPanel controls;
	
	public LayerManager(JFrame frame)
	{
		dialog = new WebDialog(frame, "Layers").center();
		dialog.setIconImage(GUIManager.ICON);
		dialog.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
		dialog.setTitle("Layers");
		dialog.getContentPane().setPreferredSize(new Dimension(200, 300));
		
		tree = new WebCheckBoxTree<Layer>((TreeModel) null);
		tree.setEditable(false);
		tree.setRecursiveChecking(false);
		tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
		tree.setShowsRootHandles(true);
		tree.addCheckStateChangeListener(this);
		tree.getSelectionModel().addTreeSelectionListener(this);
		tree.setExpandsSelectedPaths(true);
		
		JScrollPane scroll = new JScrollPane(tree);
		
		controls = new WebPanel();
		controls.setLayout(new GridLayout(0, 2));
		
		JButton newLayer = new JButton("New");
		JButton deleteLayer = new JButton("Delete");
		JButton moveUp = new JButton("Move Up");
		JButton moveDown = new JButton("Move Down");
		JButton moveOut = new JButton("Move Out");
		JButton moveIn = new JButton("Move In");
		JButton settings = new JButton("Settings");
		JButton merge = new JButton("Merge In");
		
		newLayer.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					n = rootNode;
				else
					n = (Layer) path.getLastPathComponent();
				Document doc = n.getDocument();
				if(n.isFloating())
				{
					doc.addChange(new AnchorLayer(n));
				}
				else
				{
					doc.addChange(new NewLayer(n));
				}
				Spade.main.gui.repaint();
			}
		});
		
		deleteLayer.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					return;
				else
					n = (Layer) path.getLastPathComponent();
				if(n.equals(rootNode))
					return;
				Document doc = n.getDocument();
				doc.addChange(new DeleteLayer(n));
				Spade.main.gui.repaint();
			}
		});
		
		moveUp.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					return;
				else
					n = (Layer) path.getLastPathComponent();
				if(n.equals(rootNode) || n.getPreviousSibling() == null)
					return;
				Layer parent = n.getParentLayer();
				int index = parent.getIndex(n);
				Document doc = n.getDocument();
				doc.addChange(new MoveLayer(n, parent, index - 1));
				Spade.main.gui.repaint();
			}
		});
		
		moveDown.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					return;
				else
					n = (Layer) path.getLastPathComponent();
				if(n.equals(rootNode) || n.getNextSibling() == null)
					return;
				Layer parent = n.getParentLayer();
				int index = parent.getIndex(n);
				Document doc = n.getDocument();
				doc.addChange(new MoveLayer(n, parent, index + 1));
				Spade.main.gui.repaint();
			}
		});
		
		moveOut.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					return;
				else
					n = (Layer) path.getLastPathComponent();
				if(n.equals(rootNode) || n.getPreviousSibling() == null)
					return;
				Document doc = n.getDocument();
				doc.addChange(new MoveLayer(n, (Layer) n.getPreviousSibling(), -1));
				Spade.main.gui.repaint();
			}
		});
		
		moveIn.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					return;
				else
					n = (Layer) path.getLastPathComponent();
				if(n.equals(rootNode) || n.getParentLayer().equals(rootNode))
					return;
				Layer parent = n.getParentLayer();
				Layer grandparent = parent.getParentLayer();
				int index = grandparent.getIndex(parent);
				Document doc = n.getDocument();
				doc.addChange(new MoveLayer(n, grandparent, index + 1));
				Spade.main.gui.repaint();
			}
		});
		
		merge.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					return;
				else
					n = (Layer) path.getLastPathComponent();
				if(n.isRoot())
					return;
				if(n.getChildCount() == 0)
				{
					n.getDocument().addChange(new MergeLayer(n));
					Spade.main.gui.repaint();
				}
			}
		});
		
		settings.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				TreePath path = tree.getSelectionModel().getSelectionPath();
				Layer n;
				if(path == null)
					return;
				else
					n = (Layer) path.getLastPathComponent();
				lsettings.showFor(n);
			}
		});
		
		controls.add(newLayer);
		controls.add(deleteLayer);
		controls.add(moveUp);
		controls.add(settings);
		controls.add(moveDown);
		controls.add(merge);
		controls.add(moveIn);
		controls.add(moveOut);
		
		controls.setVisible(false);
		
		WebPanel controls = new WebPanel();
		controls.setLayout(new BorderLayout());
		controls.add(this.controls, BorderLayout.CENTER);
		
		dialog.setLayout(new BorderLayout());
		dialog.add(controls, BorderLayout.SOUTH);
		dialog.add(scroll, BorderLayout.CENTER);
		
		dialog.pack();
		dialog.setResizable(true);
		
		lsettings = new LayerSettings(frame);
	}
	
	public void setVisible(Layer layer)
	{
		tree.setChecked(layer, true);
	}
	
	public boolean isVisible(Layer layer)
	{
		return tree.isChecked(layer);
	}
	
	public void updateBlendModes()
	{
		lsettings.addAllBlendModes();
	}
	
	public void setDocument(Document doc)
	{
		if(doc == null)
		{
			rootNode = null;
			model = null;
			tree.setModel(null);
			tree.revalidate();
		}
		else
		{
			setRoot(doc.getRoot());
		}
	}
	
	public void setRoot(Layer root)
	{
		rootNode = root;
		model = new DefaultTreeModel(rootNode);
		tree.setModel(model);
		tree.revalidate();
		tree.checkAll();
	}
	
	public void show()
	{
		dialog.setVisible(true);
	}
	
	public void hide()
	{
		dialog.setVisible(false);
	}
	
	public void toggle()
	{
		dialog.setVisible(!dialog.isVisible());
	}
	
	public void dispose()
	{
		dialog.dispose();
		lsettings.dispose();
	}
	
	public void select(Layer layer)
	{
		tree.setSelectedNode(layer);
	}
	
	public void redrawTree()
	{
		if(tree.getSelectionPath() != null)
		{
			// Capture the current state of the Tree node expansions.
			Vector<TreePath> paths = new Vector<TreePath>();
			
			Enumeration<TreePath> e = tree.getExpandedDescendants(new TreePath(rootNode));
			Layer selpath = (Layer) tree.getSelectionPath().getLastPathComponent();
			
			if(e != null)
				while(e.hasMoreElements())
				{
					paths.addElement(e.nextElement());
				}
			
			// Force the Tree to rebuild itself.
			DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
			model.reload();
			
			// Recover the old expanded state of the Tree.
			for(int i = 0; i < paths.size(); i++)
			{
				TreePath path = (TreePath) paths.elementAt(i);
				tree.expandPath(path);
			}
			
			tree.setSelectionPath(new TreePath(selpath.getPath()));
		}
		else
		{
			DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
			if(model != null)
				model.reload();
		}
		tree.revalidate();
		tree.repaint();
	}
	
	public boolean isVisible()
	{
		return dialog.isVisible();
	}
	
	@Override
	public void valueChanged(TreeSelectionEvent e)
	{
		if(Spade.getDocument() == null)
			return;
		if(e.getNewLeadSelectionPath() == null)
		{
			Spade.getDocument().setCurrent(Spade.getDocument().getRoot());
			controls.setVisible(false);
			return;
		}
		Layer l = (Layer) e.getNewLeadSelectionPath().getLastPathComponent();
		if(l == null)
		{
			Spade.getDocument().setCurrent(Spade.getDocument().getRoot());
			controls.setVisible(false);
		}
		else
		{
			Layer current = Spade.getDocument().getCurrent();
			if(Spade.getDocument().setCurrent(l) && current != null)
			{
				if(current.getImage().isMaskEnabled())
				{
					boolean[] mask = current.getImage().copyMask();
					current.addChange(new ClearMaskChange());
					if(Spade.main.currentTool instanceof SelectionTool)
					{
						l.addChange(new SetMaskChange(mask));
					}
				}
			}
			controls.setVisible(true);
			lsettings.updateIfVisible(l);
		}
	}
	
	@Override
	public void checkStateChanged(List<CheckStateChange<Layer>> changes)
	{
		for(CheckStateChange<Layer> c : changes)
		{
			Layer layer = c.getNode();
			layer.setVisible(c.getNewState() == CheckState.checked);
			layer.getDocument().changed(layer);
		}
	}
	
	public WebDialog getDialog()
	{
		return dialog;
	}
} public class ToolBox
{
	public final static int BUTTON_SIZE = 32;
	
	private WebToolBar toolbar;
	private ButtonGroup buttonGroup;
	
	public ToolBox()
	{
		toolbar = new WebToolBar("Toolbox", WebToolBar.VERTICAL);
		toolbar.setRollover(true);
		toolbar.setFloatable(true);
		toolbar.setFocusable(false);
		toolbar.setToolbarStyle(ToolbarStyle.standalone);
		
		buttonGroup = new ButtonGroup();
	}
	
	public void addButton(ToolBoxButton button, boolean... selected)
	{
		if(selected != null && selected.length > 0)
		{
			button.setSelected(selected[0]);
		}
		buttonGroup.add(button);
		toolbar.add(button);
	}
	
	public WebToolBar getToolbar()
	{
		return toolbar;
	}
	
	public void setSelected(Tool tool)
	{
		Enumeration<AbstractButton> e = buttonGroup.getElements();
		
		while(e.hasMoreElements())
		{
			ToolBoxButton b = (ToolBoxButton) e.nextElement();
			if(b.getTool() == tool)
			{
				b.setSelected(true);
				return;
			}
		}
	}
} @SuppressWarnings("serial")
public class DocumentButton extends WebButton implements ActionListener
{
	public Document doc;
	
	public DocumentButton(Document doc)
	{
		super("Untitled");
		this.doc = doc;
		this.addActionListener(this);
		this.setFocusable(false);
		checkName();
	}
	
	@Override
	public void actionPerformed(ActionEvent e)
	{
		Spade.setDocument(doc);
	}
	
	public void checkName()
	{
		File f = doc.getFile();
		String s;
		if(f != null)
		{
			s = f.getName();
		}
		else
		{
			s = "Untitled";
		}
		if(!doc.saved())
		{
			s += "*";
		}
		this.setText(s);
	}
} @SuppressWarnings("serial")
public class DocumentMenuItem extends WebMenuItem implements ActionListener
{
	public Document doc;
	
	public DocumentMenuItem(Document doc)
	{
		super("Untitled");
		this.doc = doc;
		this.addActionListener(this);
		checkName();
	}
	
	@Override
	public void actionPerformed(ActionEvent e)
	{
		Spade.setDocument(doc);
	}
	
	public void checkName()
	{
		File f = doc.getFile();
		String s;
		if(f != null)
		{
			s = IOUtils.relativeFrom(new File(System.getProperty("user.dir")), f);
		}
		else
		{
			s = "Untitled";
		}
		this.setText(s);
	}
} public class InfoMenuBar
{
	private WebMenu scale, saved, size, coords;
	private MemoryWatcher memoryWatcher;
	
	public WebMenuBar createInfoMenuBar()
	{
		WebMenuBar menuBar = new WebMenuBar();
		menuBar.setMenuBarStyle(MenuBarStyle.standalone);
		
		WebMenuBar left = new WebMenuBar();
		left.setUndecorated(true);
		left.setLayout(new GridLayout(1, 0));
		
		left.setBackground(PaintCanvas.TRANSPARENT);
		
		scale = new WebMenu("");
		saved = new WebMenu("");
		size = new WebMenu("");
		coords = new WebMenu("");
		
		left.add(new WebMenu());
		left.add(coords);
		left.add(size);
		left.add(scale);
		
		left.add(saved);
		
		menuBar.setLayout(new BorderLayout());
		menuBar.add(left, BorderLayout.CENTER);
		
		// Check if the MemoryWatcher should be activated.
		if(System.getProperty("DmemoryWatcherFlag") != null)
		{
			memoryWatcher = new MemoryWatcher();
			menuBar.add(memoryWatcher, BorderLayout.EAST);
		}
		
		return menuBar;
	}
	
	public void clear()
	{
		scale.setText("");
		saved.setText("");
		size.setText("");
		coords.setText("");
	}
	
	public void setScale(float scale)
	{
		this.scale.setText("Scale: " + MathUtils.round(scale * 100) + "%");
	}
	
	public void setSaved(boolean saved)
	{
		this.saved.setText("Saved: " + (saved ? "Yes" : "No"));
	}
	
	public void setSize(int w, int h)
	{
		size.setText(w + " x " + h);
	}
	
	public void setMouseCoords(int x, int y)
	{
		coords.setText("(" + x + ", " + y + ")");
	}
} public class Menu
{
	// Pixel-Grid toggle
	public static boolean GRID_ENABLED = false;
	// Dark/Light Background toggle
	public static boolean DARK_BACKGROUND = false;
	
	public static WebMenuBar createMenuBar()
	{
		// M.E.I. MenuBar
		WebMenuBar menuBar = new WebMenuBar();
		//menuBar.setMenuBarStyle(MenuBarStyle.standalone);
		
		// Main Menus
		menuBar.add(createFileMenu());
		menuBar.add(createEditMenu());
		menuBar.add(createViewMenu());
		
		// Editing Menus
		menuBar.add((Spade.main.effects.operations = new WebMenu("Image")));
		menuBar.add((Spade.main.tools.toolsMenu = new WebMenu("Tools")));
		menuBar.add((Spade.main.effects.effects = new WebMenu("Effects")));
		menuBar.add((Spade.main.effects.generators = new WebMenu("Generators")));
		
		// Info Menus
		menuBar.add(createWindowMenu());
		menuBar.add(createHelpMenu());
		
		return menuBar;
	}
	
	private static WebMenu createHelpMenu()
	{
		WebMenu help = new WebMenu("Help/Info");
		
		WebMenuItem pluginManager = new WebMenuItem("Plugin Viewer", GUIManager.getIcon("plugin_viewer"));
		pluginManager.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				PluginManager.instance.showPluginViewer();
			}
		});
		
		WebMenuItem about = new WebMenuItem("About...", GUIManager.getIcon("about"));
		about.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.about.setVisible(true);
				Spade.main.gui.about.setLocationRelativeTo(Spade.main.gui.frame);
			}
		});
		
		help.add(pluginManager);
		help.add(about);
		
		return help;
	}
	
	private static WebMenu createDialogsMenu()
	{
		WebMenu dialogs = new WebMenu("Manage Dialogs");
		
		WebMenuItem colourChooser = new WebMenuItem("Colour Chooser", GUIManager.getIcon("colour_chooser"));
		colourChooser.setAccelerator(Hotkey.F5);
		
		colourChooser.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.chooser.toggle();
			}
		});
		
		WebMenuItem layerManager = new WebMenuItem("Layer Manager", GUIManager.getIcon("layer_manager"));
		layerManager.setAccelerator(Hotkey.F6);
		
		layerManager.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.layers.toggle();
			}
		});
		
		dialogs.add(colourChooser);
		dialogs.add(layerManager);
		
		return dialogs;
	}
	
	private static WebMenu createWindowMenu()
	{
		WebMenu window = new WebMenu("Window");
		
		window.add(createDialogsMenu());
		
		return window;
	}
	
	private static WebMenu createFileMenu()
	{
		WebMenu file = new WebMenu("File");
		
		WebMenuItem newFile = new WebMenuItem("New", GUIManager.getIcon("new"));
		newFile.setAccelerator(Hotkey.CTRL_N);
		WebMenuItem load = new WebMenuItem("Open", GUIManager.getIcon("open"));
		load.setAccelerator(Hotkey.CTRL_O);
		WebMenuItem save = new WebMenuItem("Save", GUIManager.getIcon("save"));
		save.setAccelerator(Hotkey.CTRL_S);
		final WebMenuItem saveAs = new WebMenuItem("Save As", GUIManager.getIcon("save_as"));
		saveAs.setAccelerator(new HotkeyData(true, true, false, KeyEvent.VK_S));
		WebMenuItem exit = new WebMenuItem("Exit", GUIManager.getIcon("exit"));
		
		file.add(newFile);
		file.add(load);
		file.add(new WebSeparator());
		file.add(save);
		file.add(saveAs);
		file.add(new WebSeparator());
		file.add(exit);
		
		newFile.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				showNewDialog();
			}
		});
		
		load.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				showOpenMenu();
			}
		});
		
		save.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				new Thread(new Runnable()
				{
					public void run()
					{
						Spade.save(Spade.getDocument());
					}
				}).start();
			}
		});
		
		saveAs.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				new Thread(new Runnable()
				{
					public void run()
					{
						Spade.saveAs(Spade.getDocument());
					}
				}).start();
			}
		});
		
		exit.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.closeAllDocuments();
			}
		});
		
		return file;
	}
	
	public static void showOpenMenu()
	{
		final WebFileChooser chooser = new WebFileChooser(Spade.getDir());
		chooser.setFileSelectionMode(WebFileChooser.FILES_ONLY);
		chooser.setAcceptAllFileFilterUsed(false);
		chooser.setFileFilter(new ReadableFileFilter());
		
		// Add ALL the custom image-importers!
		ImageImporter.addAllImporters(chooser);
		
		int returned = chooser.showOpenDialog(new WebDialog(Spade.main.gui.frame, "Load Image"));
		
		if(returned == WebFileChooser.APPROVE_OPTION)
		{
			// If a Image takes too long, the application might crash.
			// By running the actual loading process in another thread, the AWT-Event Thread can continue working while the image is being loaded.
			new Thread(new Runnable()
			{
				@Override
				public void run()
				{
					Document doc = Document.loadFromFile(chooser.getSelectedFile());
					if(doc != null)
					{
						Spade.addDocument(doc);
						Spade.setDocument(doc);
					}
				}
			}).start();
		}
	}
	
	public static void showNewDialog()
	{
		final GridEffectDialog dialog = new GridEffectDialog(1, 1, "Create Image", null);
		
		Dimension dim = ClipboardHandler.getImageSize();
		if(dim == null)
		{
			Document current = Spade.getDocument();
			if(current != null)
			{
				dim = new Dimension(current.getWidth(), current.getHeight());
			}
			else
			{
				dim = new Dimension(512, 512);
			}
		}
		
		final JTextField width = new JTextField("" + dim.width);
		final JTextField height = new JTextField("" + dim.height);
		//width.setHorizontalAlignment(JTextField.RIGHT);
		//height.setHorizontalAlignment(JTextField.RIGHT);
		
		{
			JPanel panel = dialog.getPanel(0);
			panel.setLayout(new GridLayout(0, 2));
			
			((AbstractDocument) width.getDocument()).setDocumentFilter(new NumberFilter());
			((AbstractDocument) height.getDocument()).setDocumentFilter(new NumberFilter());
			
			width.setColumns(4);
			height.setColumns(4);
			
			JLabel wl = WeblafWrapper.createLabel("Width: ");
			wl.setHorizontalAlignment(SwingConstants.CENTER);
			
			JLabel hl = WeblafWrapper.createLabel("Height: ");
			hl.setHorizontalAlignment(SwingConstants.CENTER);
			
			panel.add(wl);
			panel.add(width);
			
			panel.add(hl);
			panel.add(height);
		}
		
		JPanel panel = dialog.getBottomPanel();
		panel.setLayout(new GridLayout(0, 2));
		JButton create = WeblafWrapper.createButton("Create");
		JButton cancel = WeblafWrapper.createButton("Cancel");
		
		panel.add(create);
		panel.add(cancel);
		
		create.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				dialog.close();
				if(width.getText().equals("") || height.getText().equals(""))
					return;
				int w = Integer.parseInt(width.getText());
				int h = Integer.parseInt(height.getText());
				if(w > Document.MAX_DIMENSION || h > Document.MAX_DIMENSION || w == 0 || h == 0)
				{
					Popup.show("Invalid Image Dimensions", "Image must have dimensions higher than 0 and no more than " + Document.MAX_DIMENSION);
				}
				else
				{
					Spade.newImage(w, h);
				}
			}
		});
		
		cancel.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				dialog.close();
			}
		});
		
		dialog.display();
	}
	
	private static WebMenu createEditMenu()
	{
		WebMenu edit = new WebMenu("Edit");
		
		WebMenuItem undo = new WebMenuItem("Undo", GUIManager.getIcon("undo"));
		undo.setAccelerator(Hotkey.CTRL_Z);
		WebMenuItem redo = new WebMenuItem("Redo", GUIManager.getIcon("redo"));
		redo.setAccelerator(Hotkey.CTRL_Y);
		
		undo.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent arg0)
			{
				if(Spade.getDocument() != null)
					Spade.getDocument().getHistory().revertChange();
			}
		});
		
		redo.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent arg0)
			{
				if(Spade.getDocument() != null)
					Spade.getDocument().getHistory().repeatChange();
			}
		});
		
		edit.add(undo);
		edit.add(redo);
		
		return edit;
	}
	
	private static WebMenu createViewMenu()
	{
		WebMenu view = new WebMenu("View");
		
		WebMenuItem zoomIn = new WebMenuItem("Zoom In", GUIManager.getIcon("zoom_inc"));
		zoomIn.setAccelerator(new HotkeyData(true, false, false, KeyEvent.VK_EQUALS));
		WebMenuItem zoomOut = new WebMenuItem("Zoom Out", GUIManager.getIcon("zoom_dec"));
		zoomOut.setAccelerator(new HotkeyData(true, false, false, KeyEvent.VK_MINUS));
		WebMenuItem zoomReset = new WebMenuItem("Reset Zoom", GUIManager.getIcon("zoom_reset"));
		zoomReset.setAccelerator(new HotkeyData(true, false, false, KeyEvent.VK_0));
		
		WebMenuItem grid = new WebMenuItem("Toggle Grid", GUIManager.getIcon("toggle_grid"));
		grid.setAccelerator(Hotkey.CTRL_G);
		WebMenuItem darkDraw = new WebMenuItem("Toggle Dark Background", GUIManager.getIcon("toggle_dark_bg"));
		darkDraw.setAccelerator(Hotkey.CTRL_B);
		
		zoomIn.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				float zoom = Spade.main.gui.canvas.getScale();
				if(zoom < 1f)
				{
					Spade.main.gui.canvas.setScale(Math.min(zoom * 2f, 1f));
				}
				else
				{
					Spade.main.gui.canvas.setScale(Math.min(zoom + 2f, 64f));
				}
			}
		});
		
		zoomOut.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				float zoom = Spade.main.gui.canvas.getScale();
				if(zoom <= 1f)
				{
					Spade.main.gui.canvas.setScale(Math.max(zoom / 2f, 1 / 32f));
				}
				else
				{
					Spade.main.gui.canvas.setScale(Math.max(zoom - 2f, 1f));
				}
			}
		});
		
		zoomReset.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				Spade.main.gui.canvas.setScale(1f);
			}
		});
		
		grid.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				GRID_ENABLED = !GRID_ENABLED;
				Spade.main.gui.repaint();
			}
		});
		
		darkDraw.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				DARK_BACKGROUND = !DARK_BACKGROUND;
				Spade.main.gui.repaint();
			}
		});
		
		view.add(zoomIn);
		view.add(zoomOut);
		view.add(zoomReset);
		view.add(new WebSeparator());
		view.add(grid);
		view.add(darkDraw);
		
		return view;
	}
	
	public static class NumberTextField extends JTextField
	{
		private static final long serialVersionUID = -8289311655265709303L;
		
		public NumberTextField(String text)
		{
			super(text);
			
			setColumns(8);
			((AbstractDocument) getDocument()).setDocumentFilter(new NumberFilter());
		}
		
		public int get()
		{
			if(this.getText().isEmpty())
				return -1;
			
			return Integer.valueOf(this.getText());
		}
	}
} public class Document
{
	public static int MAX_DIMENSION = 4096;
	
	private LinkedList<IDocChange> changes = new LinkedList<IDocChange>();
	private LinkedList<IDocChange> reverted = new LinkedList<IDocChange>();
	
	private int width, height;
	private File file;
	private Metadata info;
	private Layer root, current;
	private History history;
	
	private IChange previewChange;
	public int lowestChange;
	
	private boolean initialised;
	public boolean repaint;
	
	private ArrayList<Layer> flatmap = new ArrayList<Layer>();
	
	private Document()
	{
		this.info = new Metadata();
		this.history = new History(this);
	}
	
	public Document(int width, int height)
	{
		this();
		this.width = width;
		this.height = height;
		
		this.current = this.root = new Layer(this, new RawImage(width, height), new Metadata());
		this.flatmap.clear();
		root.constructFlatMap(flatmap);
		initialised = true;
	}
	
	public static Document loadFromFile(File f)
	{
		Document doc = new Document();
		doc.file = f;
		if(!ImageImporter.loadImage(f.getAbsolutePath(), doc))
			return null;
		doc.flatmap.clear();
		doc.root.constructFlatMap(doc.flatmap);
		doc.initialised = true;
		return doc;
	}
	
	public void reconstructFlatmap()
	{
		if(!initialised)
			return;
		Spade.main.gui.layers.redrawTree();
		this.flatmap.clear();
		root.constructFlatMap(flatmap);
	}
	
	public History getHistory()
	{
		return history;
	}
	
	public Layer getRoot()
	{
		return root;
	}
	
	public Layer getCurrent()
	{
		return current;
	}
	
	public void setRoot(Layer root)
	{
		this.current = this.root = root;
		this.reconstructFlatmap();
	}
	
	public boolean setCurrent(Layer current)
	{
		if(this.current == current)
		{
			return false;
		}
		this.current = current;
		Spade.main.gui.layers.select(current);
		return true;
	}
	
	public Metadata getMetadata()
	{
		return info;
	}
	
	public void resize(int width, int height)
	{
		this.width = width;
		this.height = height;
		this.allChanged();
		Spade.main.gui.canvas.resized(width, height);
	}
	
	public void save()
	{
		save(false);
	}
	
	public void save(boolean panic)
	{
		final String fileName = this.file.getAbsolutePath();
		
		String extension = "";
		
		int i = fileName.lastIndexOf('.');
		
		if(i > 0)
		{
			extension = fileName.substring(i + 1);
		}
		
		final ImageExporter exporter = ImageExporter.get(extension);
		
		final Document doc = this;
		
		try
		{
			exporter.save(doc, new File(fileName));
			history.save();
			Spade.main.gui.checkButtonNames();
		}
		catch(Exception e)
		{
			if(!panic)
				Popup.showException("Error Saving Document", e, "This error occured while saving the file " + file
						+ ". It may work if you try again, but if not, we apologise. Report the bug to get it fixed as soon as possible");
			else
				Popup.showException("Error Saving Document", e, "This error occured while saving the file " + file
						+ " after a crash. Sadly, we cannot recover from this.");
			return;
		}
	}
	
	public String getDir()
	{
		if(file != null)
			return this.file.getParent();
		else
			return System.getProperty("user.dir");
	}
	
	public int getWidth()
	{
		return width;
	}
	
	public int getHeight()
	{
		return height;
	}
	
	public ArrayList<Layer> getFlatMap()
	{
		return flatmap;
	}
	
	/*
	 * This should only be called when loading an image. I need to find another way to do this.
	 */
	public void setDimensions(int width, int height)
	{
		this.width = width;
		this.height = height;
	}
	
	public BufferedImage getRenderedImage()
	{
		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		{
			Graphics2D g = image.createGraphics();
			for(Layer l : flatmap)
			{
				l.render(g);
			}
			g.dispose();
		}
		return image;
	}
	
	public void preview(IChange change)
	{
		this.previewChange = change;
		this.repaint();
	}
	
	public void applyPreview()
	{
		this.getCurrent().addChange(previewChange);
		this.previewChange = null;
		this.repaint();
	}
	
	public void changed(Layer layer)
	{
		if(!initialised)
			return;
		Spade.main.gui.checkButtonNames();
		lowestChange = Math.min(lowestChange, flatmap.indexOf(layer));
		this.repaint();
	}
	
	public void allChanged()
	{
		lowestChange = 0;
		this.repaint();
	}
	
	public IChange getPreview()
	{
		return previewChange;
	}
	
	public void setFile(File file)
	{
		this.file = file;
	}
	
	public File getFile()
	{
		return file;
	}
	
	public void addChange(IDocChange change)
	{
		history.addChange(-1);
		changes.push(change);
		change.apply(this);
		this.allChanged();
	}
	
	public void addChangeSilent(IDocChange change)
	{
		changes.push(change);
		change.apply(this);
		this.allChanged();
	}
	
	public void revertChange()
	{
		if(changes.isEmpty())
		{
			return;
		}
		IDocChange change = changes.pop();
		reverted.push(change);
		change.revert(this);
		this.allChanged();
	}
	
	public void repeatChange()
	{
		if(reverted.isEmpty())
		{
			return;
		}
		IDocChange change = reverted.pop();
		changes.push(change);
		change.repeat(this);
		this.allChanged();
	}
	
	public boolean saved()
	{
		return this.file != null && this.history.isSaved();
	}
	
	public void repaint()
	{
		repaint = true;
		Spade.main.gui.repaint();
	}
	
	public void setMetadata(Metadata info)
	{
		this.info = info;
	}
} public abstract class ImageImporter extends FileFilter
{
	private static final HashMap<String, ImageImporter> importers = new HashMap<String, ImageImporter>();
	
	static
	{
		// Set this to FALSE, because it is NOT faster to use this!
		ImageIO.setUseCache(false);
	}
	
	/**
	 * Reads an Image.
	 * @throws IOException 
	 **/
	public abstract boolean load(File file, Document doc) throws IOException;
	
	public abstract String getFileExtension();
	
	@Override
	public boolean accept(File f)
	{
		if(f.isDirectory())
			return true;
		
		if(f.getAbsolutePath().endsWith("." + getFileExtension()))
			return true;
		
		return false;
	}
	
	public static void add(ImageImporter importer)
	{
		importers.put(importer.getFileExtension(), importer);
	}
	
	public static ImageImporter get(String extension)
	{
		return importers.get(extension);
	}
	
	public static boolean loadImage(String path, Document doc)
	{
		File file = new File(path);
		
		if(file.exists())
		{
			try
			{
				String fileName = file.getAbsolutePath();
				String extension = "";
				
				int i = fileName.lastIndexOf('.');
				
				if(i > 0)
				{
					extension = fileName.substring(i + 1);
				}
				
				// Get the ImageImporter
				ImageImporter importer = importers.get(extension);
				
				// If there is a custom importer for the given format, use the custom importer...
				if(importer != null)
				{
					return importer.load(file, doc);
				}
				else
				{
					RawImage image = RawImage.fromBufferedImage(ImageIO.read(file));
					doc.setDimensions(image.width, image.height);
					doc.setRoot(new Layer(doc, image, new Metadata()));
				}
			}
			catch(Exception e)
			{
				Popup.showException("Error Loading Document", e, "This error occured while loading the file " + file
						+ ". It may work if you try again, but if not, we apologise. Report the bug to get it fixed as soon as possible");
				return false;
			}
		}
		else
		{
			Popup.showException("Error Loading Document", new FileNotFoundException(file.getAbsolutePath()), "The file " + file + " could not be found");
			return false;
		}
		return true;
	}
	
	public static void addAllImporters(WebFileChooser chooser)
	{
		for(ImageImporter importer : importers.values())
		{
			chooser.addChoosableFileFilter(importer);
		}
	}
} public class ReadableFileFilter extends FileFilter
{

	@Override
	public boolean accept(File f)
	{
		String filename = f.getName();
		int i = filename.lastIndexOf(".");
		if (i > -1)
		{
    		String extension = filename.substring(i+1).toLowerCase();
    		return (!f.isDirectory() && 
    				((Arrays.asList(lower(ImageIO.getReaderFormatNames())).contains(extension)) || 
    						ImageImporter.get(extension) != null));
		} else return false;
	}

	@Override
	public String getDescription()
	{
		return "All supported file formats";
	}
	
	public String[] lower(String[] original)
	{
		String[] newArr = new String[original.length];
		for (int i = 0; i < original.length; i++)
			newArr[i] = original[i].toLowerCase();
		
		return newArr;
	}
} public class Input implements KeyListener
{
	public static boolean CTRL, SHIFT, ALT;
	public static Robot robot;
	
	private static HashMap<Integer, Character> keyCodeToChar = new HashMap<Integer, Character>();
	
	public void keyPressed(KeyEvent e)
	{
		if(e.getKeyCode() == KeyEvent.VK_CONTROL)
		{
			CTRL = true;
		}
		else if(e.getKeyCode() == KeyEvent.VK_SHIFT)
		{
			SHIFT = true;
		}
		else if(e.getKeyCode() == KeyEvent.VK_ALT)
		{
			ALT = true;
		}
		
		if(e.getKeyCode() == KeyEvent.VK_DELETE)
		{
			Spade.getDocument().getCurrent().addChange(new FillImageChange(0x00000000));
		}
		
		int MOVE = 1;
		
		if(e.isControlDown())
		{
			MOVE = 8;
			if(!e.isShiftDown())
			{
				if(e.getKeyCode() == KeyEvent.VK_Q)
				{
					if(Spade.getDocument() != null)
					{
						Spade.closeDocument(Spade.getDocument());
					}
					else
					{
						Spade.close();
					}
				}
				else if(Spade.getDocument() != null)
				{
					if(e.getKeyCode() == KeyEvent.VK_D)
					{
						Spade.getDocument().getCurrent().addChange(new ClearMaskChange());
					}
					else if(e.getKeyCode() == KeyEvent.VK_A)
					{
						Spade.getDocument().getCurrent().addChange(new FillMaskChange(MaskMode.ADD));
					}
					else if(e.getKeyCode() == KeyEvent.VK_I)
					{
						Spade.getDocument().getCurrent().addChange(new FillMaskChange(MaskMode.XOR));
					}
					else if(e.getKeyCode() == KeyEvent.VK_F)
					{
						Document doc = Spade.getDocument();
						if(!doc.getCurrent().isFloating())
						{
							doc.addChange(new FloatLayer(Spade.getDocument().getCurrent(), e.isAltDown()));
						}
					}
					else if(e.getKeyCode() == KeyEvent.VK_M)
					{
						Layer current = Spade.getDocument().getCurrent();
						if(current.getChildCount() == 0)
							Spade.getDocument().addChange(new MergeLayer(current));
					}
					else if(e.getKeyCode() == KeyEvent.VK_C)
					{
						ClipboardHandler.setImage(Spade.getDocument().getCurrent().getImage());
					}
					else if(e.getKeyCode() == KeyEvent.VK_X)
					{
						ClipboardHandler.setImage(Spade.getDocument().getCurrent().getImage());
						Spade.getDocument().getCurrent().addChange(new FillImageChange(0x00000000));
					}
					else if(e.getKeyCode() == KeyEvent.VK_V)
					{
						Document doc = Spade.getDocument();
						RawImage image = ClipboardHandler.getImage();
						if(image != null)
						{
							if(doc.getCurrent().isFloating())
							{
								doc.addChange(new MergeLayer(doc.getCurrent()));
							}
							// Fun hack.
							if(image.width != doc.getWidth() || image.height != doc.getHeight())
							{
								image = new ResizeCanvasChange(doc.getWidth(), doc.getHeight()).apply(image);
							}
							doc.addChange(new NewLayer(doc.getCurrent(), image, "Floating Layer").floating());
						}
					}
				}
			}
		}
		Point p = MouseInfo.getPointerInfo().getLocation();
		
		MOVE = (int) (MOVE * Spade.main.gui.canvas.getScale());
		
		if(e.getKeyCode() == KeyEvent.VK_UP)
		{
			robot.mouseMove(p.x, p.y - MOVE);
		}
		else if(e.getKeyCode() == KeyEvent.VK_DOWN)
		{
			robot.mouseMove(p.x, p.y + MOVE);
		}
		else if(e.getKeyCode() == KeyEvent.VK_LEFT)
		{
			robot.mouseMove(p.x - MOVE, p.y);
		}
		else if(e.getKeyCode() == KeyEvent.VK_RIGHT)
		{
			robot.mouseMove(p.x + MOVE, p.y);
		}
		
		if(e.getKeyCode() == KeyEvent.VK_SPACE)
		{
			robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
		}
	}
	
	public void keyReleased(KeyEvent e)
	{
		if(e.getKeyCode() == KeyEvent.VK_CONTROL)
		{
			CTRL = false;
		}
		else if(e.getKeyCode() == KeyEvent.VK_SHIFT)
		{
			SHIFT = false;
		}
		else if(e.getKeyCode() == KeyEvent.VK_ALT)
		{
			ALT = false;
		}
		else if(e.getKeyCode() == KeyEvent.VK_SPACE)
		{
			robot.mouseRelease(InputEvent.BUTTON1_MASK);
		}
	}
	
	public void keyTyped(KeyEvent e)
	{
		
	}
	
	static
	{
		keyCodeToChar.put(KeyEvent.VK_A, 'A');
		keyCodeToChar.put(KeyEvent.VK_B, 'B');
		keyCodeToChar.put(KeyEvent.VK_C, 'C');
		keyCodeToChar.put(KeyEvent.VK_D, 'D');
		keyCodeToChar.put(KeyEvent.VK_E, 'E');
		keyCodeToChar.put(KeyEvent.VK_F, 'F');
		keyCodeToChar.put(KeyEvent.VK_G, 'G');
		keyCodeToChar.put(KeyEvent.VK_H, 'H');
		keyCodeToChar.put(KeyEvent.VK_I, 'I');
		keyCodeToChar.put(KeyEvent.VK_J, 'J');
		keyCodeToChar.put(KeyEvent.VK_K, 'K');
		keyCodeToChar.put(KeyEvent.VK_L, 'L');
		keyCodeToChar.put(KeyEvent.VK_M, 'M');
		keyCodeToChar.put(KeyEvent.VK_N, 'N');
		keyCodeToChar.put(KeyEvent.VK_O, 'O');
		keyCodeToChar.put(KeyEvent.VK_P, 'P');
		keyCodeToChar.put(KeyEvent.VK_Q, 'Q');
		keyCodeToChar.put(KeyEvent.VK_R, 'R');
		keyCodeToChar.put(KeyEvent.VK_S, 'S');
		keyCodeToChar.put(KeyEvent.VK_T, 'T');
		keyCodeToChar.put(KeyEvent.VK_U, 'U');
		keyCodeToChar.put(KeyEvent.VK_V, 'V');
		keyCodeToChar.put(KeyEvent.VK_W, 'W');
		keyCodeToChar.put(KeyEvent.VK_X, 'X');
		keyCodeToChar.put(KeyEvent.VK_Y, 'Y');
		keyCodeToChar.put(KeyEvent.VK_Z, 'Z');
		
		try
		{
			robot = new Robot();
		}
		catch(AWTException e)
		{
			e.printStackTrace();
		}
	}
} public class Spade
{
	public static final String REPO_URL = "https://github.com/Spade-Editor/Spade";
	
	// Major.Minor + optional letter for releases. The letter is for tiny revisions, such as fixing bugs that slipped through.
	// Major.Minor-RC.# for Release Candidates (builds that may be promoted to releases).
	// Major.Minor-Beta for beta builds.
	// Major.Minor-Alpha for alpha builds.
	// Major.Minor-Dev for development builds.
	// Eg: 1.3b is the 2nd revision of version 1.3
	
	// Stable for stable post-1.0 versions
	// Beta for new completed features.
	// Development for under-development new features.
	
	public static final String VERSION_STRING = "0.17.0-dev";
	public static final Version VERSION = Version.parse(VERSION_STRING);
	public static final String RELEASED = "13-03-2015";
	
	/* Add/Remove the stars on the following lines to change the build type string.
	//*/public static final String BUILD_TYPE = "Development";
	//*/public static final String BUILD_TYPE = "Alpha"; // I don't know if we'll ever use alphas.
	//*/public static final String BUILD_TYPE = "Beta";
	//*/public static final String BUILD_TYPE = "Release Candidate";
	//*/public static final String BUILD_TYPE = "Stable";
	
	public static boolean debug, localPlugins = true, globalPlugins = true;
	public static Spade main = new Spade();
	public static URL questionMarkURL = Spade.class.getResource("/res/icons/questionmark.png");
	
	public GUIManager gui;
	public PluginManager pluginManager;
	
	private Document document;
	
	public Tool currentTool;
	
	public Tools tools;
	public Effects effects;
	
	public static int leftColour = 0xff000000;
	public static int rightColour = 0xffffffff;
	
	private static HashMap<Character, Tool> toolMap = new HashMap<Character, Tool>();
	
	private static HashMap<Character, Effect> effectMap = new HashMap<Character, Effect>();
	
	public void launch(final ArrayList<File> toOpen) throws InvocationTargetException, InterruptedException
	{
		pluginManager = PluginManager.instance;
		
		HistoryIO.init();
		
		// Order is important. Prefer local (./spade) plugins to global (~/.spade/plugins).
		if(localPlugins)
			pluginManager.addPluginDirectory(new File(IOUtils.assemblePath(IOUtils.jarDir(), "plugins")));
		if(globalPlugins)
			pluginManager.addPluginDirectory(new File(IOUtils.assemblePath(System.getProperty("user.home"), ".spade", "plugins")));
		pluginManager.loadPluginFiles();
		
		tools = new Tools();
		effects = new Effects();
		
		SwingUtilities.invokeAndWait(new Runnable()
		{
			@Override
			public void run()
			{
				// Try to catch everything and recover enough to save.
				try
				{
					gui = new GUIManager();
					gui.init();
					
					setLeftColour(0xff000000, false);
					setRightColour(0xffffffff, false);
					
					tools.init();
					effects.init(); // Doesn't actually do anything
					pluginManager.loadPlugins();
					setTool(currentTool);
					for(File f : toOpen)
					{
						if(debug)
							System.out.println("Opening File " + f.getPath());
						Document d = Document.loadFromFile(f);
						if(d != null)
							Spade.addDocument(d);
					}
					ArrayList<Document> documents = gui.getDocuments();
					if(!documents.isEmpty())
						Spade.setDocument(documents.get(0));
					
					gui.frame.requestFocus();
				}
				catch(Exception e)
				{
					panic(e);
				}
			}
		});
	}
	
	public static void newImage(final int width, final int height)
	{
		Document doc = new Document(width, height);
		Spade.addDocument(doc);
		Spade.setDocument(doc);
	}
	
	public static void addTool(Character key, Tool tool)
	{
		toolMap.put(Character.toLowerCase(key), tool);
	}
	
	public static Tool getTool(Character key)
	{
		return toolMap.get(Character.toLowerCase(key));
	}
	
	public static void addEffect(Character key, Effect op)
	{
		effectMap.put(Character.toLowerCase(key), op);
	}
	
	public static Effect getEffect(Character key)
	{
		return effectMap.get(Character.toLowerCase(key));
	}
	
	public static boolean setTool(Tool tool)
	{
		if(main.currentTool == tool)
			return false;
		Input.CTRL = false;
		Input.ALT = false;
		Input.SHIFT = false;
		main.currentTool = tool;
		main.tools.toolbox.setSelected(tool);
		return true;
	}
	
	public static boolean save(Document doc)
	{
		if(doc == null)
		{
			return false;
		}
		if(doc.getFile() != null)
		{
			doc.save();
			return true;
		}
		else
			return saveAs(doc);
	}
	
	public static boolean saveAs(Document doc)
	{
		if(doc == null)
		{
			return false;
		}
		WebFileChooser chooser = new WebFileChooser(doc.getDir());
		chooser.setFileSelectionMode(WebFileChooser.FILES_ONLY);
		chooser.setAcceptAllFileFilterUsed(false);
		
		ImageExporter.addAllExporters(chooser);
		
		FileFilter allFilter = new FileFilter()
		{
			@Override
			public boolean accept(File f)
			{
				if(f.isDirectory())
					return true;
				String fileName = f.getAbsolutePath();
				int i = fileName.lastIndexOf('.');
				if(i < 0)
					return false;
				
				return ImageExporter.get(fileName.substring(i + 1)) != null;
			}
			
			@Override
			public String getDescription()
			{
				return "All supported import formats";
			}
		};
		chooser.setFileFilter(allFilter);
		
		int returned = chooser.showSaveDialog(new WebDialog(main.gui.frame, "Save Image"));
		
		if(returned == WebFileChooser.APPROVE_OPTION)
		{
			// FIXME: The filechooser doesn't seem to change the filter to the one the user selected.
			// It always seems to fall through to the else branch.
			if(chooser.getFileFilter() instanceof ImageExporter)
			{
				ImageExporter format = (ImageExporter) chooser.getFileFilter();
				File file = chooser.getSelectedFile();
				
				String fileName = file.getAbsolutePath();
				
				if(fileName.endsWith("." + format.getFileExtension()))
				{
					// Do nothing.
					doc.setFile(file);
				}
				else
				{
					// Put the format at the end of the File-Name!
					fileName += "." + format.getFileExtension();
					doc.setFile(new File(fileName));
				}
			}
			else
			{
				File file = chooser.getSelectedFile();
				if(!allFilter.accept(file))
				{
					file = new File(file.getAbsolutePath() + ".png");
				}
				doc.setFile(file);
			}
			
			if(doc == main.document)
				main.gui.setTitle(doc.getFile().getName());
			doc.save();
			return true;
		}
		return false;
	}
	
	public void setLeftColour(int c, boolean checked)
	{
		if(!checked)
		{
			gui.chooser.setColour(c, this, true);
		}
		Spade.leftColour = c;
	}
	
	public int getLeftColour()
	{
		return Spade.leftColour;
	}
	
	public void setRightColour(int c, boolean checked)
	{
		if(!checked)
		{
			gui.chooser.setColour(c, this, false);
		}
		Spade.rightColour = c;
	}
	
	public int getRightColour()
	{
		return Spade.rightColour;
	}
	
	/**
	 * @param mouseButton The mouse-button to get the color for.
	 * @return The color assigned to the given MouseButton.
	 */
	public int getColor(int mouseButton)
	{
		// BUTTON1 (LEFT): left
		// BUTTON2 (MIDDLE): Color.BLACK
		// BUTTON3 (RIGHT): right
		return mouseButton == MouseEvent.BUTTON1 ? Spade.leftColour : (mouseButton == MouseEvent.BUTTON3 ? Spade.rightColour : 0xFF000000);
	}
	
	public static void launchWithPlugins(String[] args, Plugin... plugins)
	{
		for(Plugin p : plugins)
		{
			if(p.getInfo() == null)
			{
				try
				{
					BufferedReader in = new BufferedReader(new InputStreamReader(p.getClass().getResourceAsStream("/plugin.info")));
					p.setInfo(PluginManager.loadPluginInfo(in));
				}
				catch(IOException e)
				{
					e.printStackTrace();
				}
			}
			p.getInfo().set("location", "Class: " + p.getClass().getCanonicalName());
			PluginManager.instance.registerPlugin(p);
		}
		launch(args);
	}
	
	public static void launch(String[] args)
	{
		IOUtils.setMainClass(Spade.class);
		
		ArrayList<File> open = new ArrayList<File>();
		
		System.setProperty("DlafClassName", "");
		
		StringBuilder argsDebug = new StringBuilder();
		
		// Go through ALL the arguments and...
		for(String arg : args)
		{
			argsDebug.append("Picked up argument: " + arg + "\n");
			
			if(arg.equals("-v"))
			{
				// Print version string and exit.
				System.out.println(VERSION);
				System.exit(0);
			}
			else if(arg.equals("--version"))
			{
				// Print detailed version info and exit.
				System.out.println("Spade v" + VERSION);
				System.out.println("Version Released: " + RELEASED);
				System.out.println("Built Type: " + BUILD_TYPE);
				System.exit(0);
			}
			else if(arg.equals("--print-jar-path"))
			{
				// Print the absolute path of the jar and exit.
				System.out.println(IOUtils.jarPath());
				System.exit(0);
			}
			else if(arg.equals("--show-memory-watcher"))
			{
				// ...If the arguments contain the DmemoryWatcherFlag flag, set the property to true to enable the MemoryWatcher.
				System.setProperty("DmemoryWatcherFlag", "true");
			}
			else if(arg.startsWith("--look-and-feel="))
			{
				System.setProperty("DlafClassName", arg.substring(16));
			}
			else if(arg.equals("--debug"))
			{
				debug = true;
			}
			else if(arg.equals("--no-global-plugins"))
			{
				globalPlugins = false;
			}
			else if(arg.equals("--no-local-plugins"))
			{
				localPlugins = false;
			}
			else if(arg.equals("--help"))
			{
				System.out.println("Spade v" + VERSION);
				System.out.println("Version Released: " + RELEASED);
				System.out.println("Built Type: " + BUILD_TYPE);
				System.out.println();
				System.out.println("Copyright 2013-2014 HeroesGrave and Other Spade Developers");
				System.out.println();
				System.out.println("For more info, to report issues, make suggestions, or contribute");
				System.out.println("please visit " + REPO_URL);
				System.out.println();
				System.out.println("Command-Line Options:");
				System.out.println();
				System.out.println("    Information:");
				System.out.println("    --help:                  Print this text and exit");
				System.out.println("    -v                       Print the raw version id and exit");
				System.out.println("    --version                Print more detailed version info and exit");
				System.out.println();
				System.out.println("    Plugin Settings:");
				System.out.println("    --no-local-plugins       Disable Local Plugins (./plugins/)");
				System.out.println("    --no-global-plugins      Disable Global Plugins (~/.spade/.plugins/)");
				System.out.println();
				System.out.println("    UI Settings:");
				System.out.println("    --look-and-feel=[LAF]    Specify the 'Look And Feel' to use");
				System.out.println("    --show-memory-watcher    Enable the memory usage watcher");
				System.out.println();
				System.out.println("    Debug Switches:");
				System.out.println("    --debug                  Enable debugging messages");
				System.out.println("    --debug-timings          Print rendering times");
				System.out.println();
				System.out.println("Any other arguments will be read as files to open on startup.");
				System.exit(0);
			}
			else
			{
				File f = new File(arg);
				
				if(f.exists() && f.isFile() && !f.isHidden())
				{
					open.add(f);
				}
			}
			
			/// XXX: Expand here by adding more debugging options and system flags!
		}
		
		if(debug)
			System.out.println(argsDebug);
		
		// Try to catch all exceptions and recover enough to save
		try
		{
			// Finally Launch Spade!
			main.launch(open);
		}
		catch(Exception e)
		{
			panic(e);
		}
	}
	
	public static Version getVersion()
	{
		return VERSION;
	}
	
	public static void addDocument(Document doc)
	{
		main.gui.addDocument(doc);
	}
	
	public static void setDocument(Document doc)
	{
		main.document = doc;
		main.gui.setDocument(main.document);
	}
	
	public static Document getDocument()
	{
		return main.document;
	}
	
	public static void closeAllDocuments()
	{
		Spade.closeDocument(Spade.getDocument(), new Callback()
		{
			@Override
			public void callback()
			{
				Spade.closeAllDocuments();
			}
		});
	}
	
	public static void closeDocument(Document doc)
	{
		closeDocument(doc, new Callback()
		{
			@Override
			public void callback()
			{
			}
		});
	}
	
	public static void closeDocument(final Document doc, final Callback callback)
	{
		if(main.gui.getDocuments().isEmpty())
		{
			Spade.close();
		}
		final int index = main.gui.getDocuments().indexOf(doc);
		main.gui.tryRemove(doc, new Callback()
		{
			@Override
			public void callback()
			{
				ArrayList<Document> documents = main.gui.getDocuments();
				if(main.gui.getDocuments().isEmpty())
				{
					setDocument(null);
				}
				if(doc == main.document)
				{
					if(index == 0)
						setDocument(documents.get(0));
					else
						setDocument(documents.get(index - 1));
				}
				callback.callback();
			}
		});
	}
	
	public static void close()
	{
		UserPreferences.savePrefs(main.gui.frame, main.gui.chooser, main.gui.layers);
		main.pluginManager.dispose();
		main.gui.frame.dispose();
		System.exit(0);
	}
	
	public static String getDir()
	{
		if(main.document != null)
			return main.document.getDir();
		return System.getProperty("user.dir");
	}
	
	public static void panic(Exception e)
	{
		try {
			ArrayList<Document> documents = main.gui.getDocuments();
			for(Document doc : documents)
			{
				File f = doc.getFile();
				if(f == null)
					continue;
				String name = "~" + f.getName() + ".bck";
				int i = 0;
				while(f.exists())
				{
					f = new File(f.getParentFile(), name + "." + i++);
				}
				doc.setFile(f);
				doc.save(true);
			}
			main.pluginManager.dispose();
			main.gui.frame.dispose();
		}
		catch(Exception e2)
		{
			e2.printStackTrace();
			System.err.println("Exception encountered while handling an exception!\nYou will not go to space today.\nOriginal Error:");
			e.printStackTrace();
			System.exit(-1);
		}
		Popup.showException("Spade has crashed", e, "");
		System.exit(-1);
	}
} public class UserPreferences
{
	private static final String WINDOW_WIDTH = "window.width";
	private static final String WINDOW_HEIGHT = "window.height";
	private static final String WINDOW_MAXIMIZED = "window.maximized";
	private static final String COLOUR_PICKER_X = "colour.x";
	private static final String COLOUR_PICKER_Y = "colour.y";
	private static final String COLOUR_PICKER_MODE = "colour.mode";
	private static final String COLOUR_PICKER_VISIBLE = "colour.visible";
	private static final String LAYERS_X = "layers.x";
	private static final String LAYERS_Y = "layers.y";
	private static final String LAYERS_WIDTH = "layers.width";
	private static final String LAYERS_HEIGHT = "layers.height";
	private static final String LAYERS_VISIBLE = "layers.visible";
	private static final String BACKGROUND_DARK = "background.dark";
	
	private static int windowWidth, windowHeight;
	private static int layersX, layersY, layersWidth, layersHeight;
	private static int colourPickerX, colourPickerY, colourPickerMode;
	
	public static void loadPrefs(JFrame frame, ColourChooser chooser, LayerManager layers)
	{
		Preferences prefs = Preferences.userNodeForPackage(UserPreferences.class);
		
		if(prefs.getBoolean(WINDOW_MAXIMIZED, false))
		{
			frame.setExtendedState(Frame.MAXIMIZED_BOTH);
		}
		windowWidth = prefs.getInt(WINDOW_WIDTH, 800);
		windowHeight = prefs.getInt(WINDOW_HEIGHT, 600);
		colourPickerX = prefs.getInt(COLOUR_PICKER_X, 0);
		colourPickerY = prefs.getInt(COLOUR_PICKER_Y, 0);
		colourPickerMode = prefs.getInt(COLOUR_PICKER_MODE, 2);
		layersX = prefs.getInt(LAYERS_X, 0);
		layersY = prefs.getInt(LAYERS_Y, 0);
		layersWidth = prefs.getInt(LAYERS_WIDTH, 200);
		layersHeight = prefs.getInt(LAYERS_HEIGHT, 300);
		
		frame.setSize(windowWidth, windowHeight);
		frame.setLocationRelativeTo(null);
		
		chooser.setLocation(colourPickerX, colourPickerY);
		chooser.setMode(colourPickerMode);
		if(prefs.getBoolean(COLOUR_PICKER_VISIBLE, false))
		{
			chooser.setVisible(true);
		}
		
		layers.dialog.setVisible(prefs.getBoolean(LAYERS_VISIBLE, false));
		layers.dialog.setBounds(layersX, layersY, layersWidth, layersHeight);
		
		Menu.DARK_BACKGROUND = prefs.getBoolean(BACKGROUND_DARK, false);
	}
	
	public static void savePrefs(JFrame frame, ColourChooser chooser, LayerManager layers)
	{
		Preferences prefs = Preferences.userNodeForPackage(UserPreferences.class);
		
		if((frame.getExtendedState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH)
		{
			prefs.putBoolean(WINDOW_MAXIMIZED, true);
		}
		else
		{
			windowWidth = frame.getWidth();
			windowHeight = frame.getHeight();
			prefs.putInt(WINDOW_WIDTH, windowWidth);
			prefs.putInt(WINDOW_HEIGHT, windowHeight);
			prefs.putBoolean(WINDOW_MAXIMIZED, false);
		}
		colourPickerX = chooser.getX();
		colourPickerY = chooser.getY();
		colourPickerMode = chooser.getMode();
		prefs.putInt(COLOUR_PICKER_X, colourPickerX);
		prefs.putInt(COLOUR_PICKER_Y, colourPickerY);
		prefs.putInt(COLOUR_PICKER_MODE, colourPickerMode);
		prefs.putBoolean(COLOUR_PICKER_VISIBLE, chooser.isVisible());
		
		layersX = layers.dialog.getX();
		layersY = layers.dialog.getY();
		layersWidth = layers.dialog.getWidth();
		layersHeight = layers.dialog.getHeight();
		prefs.putInt(LAYERS_X, layersX);
		prefs.putInt(LAYERS_Y, layersY);
		prefs.putInt(LAYERS_WIDTH, layersWidth);
		prefs.putInt(LAYERS_HEIGHT, layersHeight);
		prefs.putBoolean(LAYERS_VISIBLE, layers.isVisible());
		
		prefs.putBoolean(COLOUR_PICKER_VISIBLE, chooser.isVisible());
		
		prefs.putBoolean(BACKGROUND_DARK, Menu.DARK_BACKGROUND);
	}
} public class IOUtils
{
	private static Class<?> MAIN_CLASS;
	
	private IOUtils()
	{
		
	}
	
	public static void setMainClass(Class<?> c)
	{
		MAIN_CLASS = c;
	}
	
	public static String jarDir()
	{
		try
		{
			return new File(MAIN_CLASS.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();
		}
		catch(URISyntaxException e)
		{
			e.printStackTrace();
			return null;
		}
	}
	
	public static String jarPath()
	{
		try
		{
			return MAIN_CLASS.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
		}
		catch(URISyntaxException e)
		{
			e.printStackTrace();
			return null;
		}
	}
	
	public static String assemblePath(String... path)
	{
		String fullPath = "";
		for(String p : path)
		{
			fullPath += File.separator + p;
		}
		return fullPath;
	}
	
	public static String fileToString(String path)
	{
		StringBuffer sBuffer = new StringBuffer();
		try
		{
			BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path)));
			char[] buffer = new char[1024];
			
			int c;
			
			while((c = reader.read(buffer, 0, buffer.length)) != -1)
			{
				sBuffer.append(buffer, 0, c);
			}
			reader.close();
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		
		return sBuffer.toString();
	}
	
	public static String fileToStringOneLine(String path)
	{
		String string = new String();
		try
		{
			BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path)));
			
			String line;
			while((line = reader.readLine()) != null)
			{
				string += line;
			}
			reader.close();
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		
		return string;
	}
	
	public static String relativeFrom(File from, File to)
	{
		return from.toURI().relativize(to.toURI()).getPath();
	}
} public interface Callback
{
	public void callback();
}
import heroesgrave.spade.gui.colorchooser.ColourChooser;
import heroesgrave.spade.gui.colorchooser.event.ColourListener;
import heroesgrave.spade.gui.dialogs.AboutDialog;
import heroesgrave.spade.gui.dialogs.LayerManager;
import heroesgrave.spade.gui.dialogs.ToolBox;
import heroesgrave.spade.gui.menus.DocumentButton;
import heroesgrave.spade.gui.menus.DocumentMenuItem;
import heroesgrave.spade.gui.menus.InfoMenuBar;
import heroesgrave.spade.gui.menus.Menu;
import heroesgrave.spade.image.Document;
import heroesgrave.spade.io.ImageImporter;
import heroesgrave.spade.io.ReadableFileFilter;
import heroesgrave.spade.main.Input;
import heroesgrave.spade.main.Spade;
import heroesgrave.spade.main.UserPreferences;
import heroesgrave.utils.io.IOUtils;
import heroesgrave.utils.misc.Callback;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.WindowConstants;
import com.alee.laf.WebLookAndFeel;
import com.alee.laf.button.WebButton;
import com.alee.laf.menu.WebMenu;
import com.alee.laf.menu.WebMenuBar;
import com.alee.laf.menu.WebMenuItem;
import com.alee.laf.panel.WebPanel;
import com.alee.laf.rootpane.WebDialog;
import com.alee.laf.rootpane.WebFrame;
import com.alee.laf.scroll.WebScrollPane;
import com.alee.laf.separator.WebSeparator;
// {LICENSE}
/*
 * Copyright 2013-2015 HeroesGrave and other Spade developers.
 * 
 * This file is part of Spade
 * 
 * Spade 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 heroesgrave.spade.gui;





public class GUIManager
{
	public static final BufferedImage ICON;
	public WebFrame frame;
	private JPanel panel;
	private WebPanel menus;
	public PaintCanvas canvas;
	
	public InfoMenuBar info;
	
	private WebMenuBar menuBar, documentBar, infoBar;
	
	private WebPanel documents;
	private WebMenu documentsDrop;
	
	public WebScrollPane scroll;
	public ColourChooser chooser;
	public LayerManager layers;
	
	public ToolBox toolBox;
	
	public AboutDialog about;
	
	static
	{
		BufferedImage image = null;
		try
		{
			image = ImageIO.read(GUIManager.class.getResource("/res/icon.png"));
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		ICON = image;
	}
	
	public GUIManager()
	{
		setLAF();
	}
	
	public void addDocument(Document doc)
	{
		documents.add(new DocumentButton(doc));
		documents.revalidate();
		documentsDrop.add(new DocumentMenuItem(doc));
		if(documents.getComponentCount() > 0)
		{
			infoBar.setVisible(true);
		}
	}
	
	public void tryRemove(final Document doc, final Callback callback)