Subject: [EGIT PATCH 06/11] Implement basic customizable label decorations with preferences

From: Tor Arne Vestbø <[email-masked]>
Date: Thu, 5 Feb 2009 02:00:13 +0100

Currently the only binding available is the resource name, but
this commit enables a framework for adding more bindings.

Signed-off-by: Tor Arne Vestbø <[email-masked]>
---
 org.spearce.egit.ui/plugin.properties              |    1 +
 org.spearce.egit.ui/plugin.xml                     |   12 +-
 .../src/org/spearce/egit/ui/Activator.java         |   16 +
 .../egit/ui/PluginPreferenceInitializer.java       |    8 +
 .../src/org/spearce/egit/ui/UIPreferences.java     |    9 +
 .../src/org/spearce/egit/ui/UIText.java            |   63 ++-
 .../egit/ui/internal/actions/BranchAction.java     |    4 +-
 .../egit/ui/internal/actions/Disconnect.java       |    4 +-
 .../egit/ui/internal/actions/ResetAction.java      |    4 +-
 .../decorators/GitLightweightDecorator.java        |  538 ++++++++++++++
 .../internal/decorators/GitResourceDecorator.java  |  454 ------------
 .../internal/decorators/IDecoratableResource.java  |   31 +
 .../preferences/GitDecoratorPreferencePage.java    |  735 ++++++++++++++++++++
 .../src/org/spearce/egit/ui/uitext.properties      |   25 +-
 14 files changed, 1438 insertions(+), 466 deletions(-)
 create mode 100644 org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java
 delete mode 100644 org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitResourceDecorator.java
 create mode 100644 org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java
 create mode 100644 org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java

diff --git a/org.spearce.egit.ui/plugin.properties b/org.spearce.egit.ui/plugin.properties
index fa043f1..58b879f 100644
--- a/org.spearce.egit.ui/plugin.properties
+++ b/org.spearce.egit.ui/plugin.properties
@@ -64,3 +64,4 @@ Theme_CommitMessageFont_description=This font is used to show a commit message.
 GitPreferences_name=Git
 GitPreferences_HistoryPreferencePage_name=History
 GitPreferences_WindowCachePreferencePage_name=Window Cache
+GitPreferences_DecoratorPreferencePage_name=Label Decorations
diff --git a/org.spearce.egit.ui/plugin.xml b/org.spearce.egit.ui/plugin.xml
index 869108c..2f23559 100644
--- a/org.spearce.egit.ui/plugin.xml
+++ b/org.spearce.egit.ui/plugin.xml
@@ -200,6 +200,14 @@
 		  id="org.spearce.egit.ui.keyword.git">
 	    </keywordReference>
       </page>
+	  <page name="%GitPreferences_DecoratorPreferencePage_name"
+	    category="org.spearce.egit.ui.GitPreferences"
+            class="org.spearce.egit.ui.internal.preferences.GitDecoratorPreferencePage"
+	    id="org.spearce.egit.ui.internal.preferences.GitDecoratorPreferencePage" >
+	    <keywordReference
+		  id="org.spearce.egit.ui.keyword.git">
+	    </keywordReference>
+      </page>
    </extension>
 
    <extension point="org.eclipse.ui.propertyPages">
@@ -233,10 +241,10 @@
             lightweight="true"
             adaptable="true"
             label="%Decorator_name"
-            class="org.spearce.egit.ui.internal.decorators.GitResourceDecorator"
+            class="org.spearce.egit.ui.internal.decorators.GitLightweightDecorator"
             state="true"
             location="BOTTOM_RIGHT"
-            id="org.spearce.egit.ui.internal.decorators.GitResourceDecorator">
+            id="org.spearce.egit.ui.internal.decorators.GitLightweightDecorator">
             <enablement>
               <objectClass name="org.eclipse.core.resources.IResource"/>
             </enablement>
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java
index d4a9e8e..9d03c70 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/Activator.java
@@ -33,6 +33,7 @@
 import org.eclipse.jface.util.PropertyChangeEvent;
 import org.eclipse.jsch.core.IJSchService;
 import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.widgets.Display;
 import org.eclipse.ui.plugin.AbstractUIPlugin;
 import org.eclipse.ui.themes.ITheme;
 import org.osgi.framework.BundleContext;
@@ -80,6 +81,21 @@ public static String getPluginId() {
 	}
 
 	/**
+	 * Returns the standard display to be used. The method first checks, if the
+	 * thread calling this method has an associated display. If so, this display
+	 * is returned. Otherwise the method returns the default display.
+	 * 
+	 * @return the display to use
+	 */
+	public static Display getStandardDisplay() {
+		Display display = Display.getCurrent();
+		if (display == null) {
+			display = Display.getDefault();
+		}
+		return display;
+	}
+
+	/**
 	 * Instantiate an error exception.
 	 * 
 	 * @param message
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java
index bb7381f..79c2665 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/PluginPreferenceInitializer.java
@@ -35,6 +35,14 @@ public void initializeDefaultPreferences() {
 		prefs.setDefault(UIPreferences.RESOURCEHISTORY_SHOW_REV_COMMENT, true);
 		prefs.setDefault(UIPreferences.RESOURCEHISTORY_SHOW_TOOLTIPS, false);
 
+		prefs.setDefault(UIPreferences.DECORATOR_FILETEXT_DECORATION,
+				UIText.DecoratorPreferencesPage_fileFormatDefault);
+		prefs.setDefault(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION,
+				UIText.DecoratorPreferencesPage_folderFormatDefault);
+		prefs.setDefault(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION,
+				UIText.DecoratorPreferencesPage_projectFormatDefault);
+		prefs.setDefault(UIPreferences.DECORATOR_CALCULATE_DIRTY, true);
+
 		w = new int[] { 500, 500 };
 		UIPreferences.setDefault(prefs,
 				UIPreferences.RESOURCEHISTORY_GRAPH_SPLIT, w);
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java
index 5ab6b25..a6168a0 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIPreferences.java
@@ -52,6 +52,15 @@
 	/** */
 	public final static String THEME_CommitMessageFont = "org.spearce.egit.ui.CommitMessageFont";
 
+	/** */
+	public final static String DECORATOR_CALCULATE_DIRTY = "decorator_calculate_dirty";
+	/** */
+	public final static String DECORATOR_FILETEXT_DECORATION = "decorator_filetext_decoration";
+	/** */
+	public final static String DECORATOR_FOLDERTEXT_DECORATION = "decorator_foldertext_decoration";
+	/** */
+	public final static String DECORATOR_PROJECTTEXT_DECORATION = "decorator_projecttext_decoration";
+
 	/**
 	 * Get the preference values associated with a fixed integer array.
 	 * 
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java
index 7a7d3ef..23498c8 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/UIText.java
@@ -446,9 +446,6 @@
 	public static String RefSpecPage_annotatedTagsNoTags;
 
 	/** */
-	public static String Decorator_failedLazyLoading;
-
-	/** */
 	public static String QuickDiff_failedLoading;
 
 	/** */
@@ -913,6 +910,66 @@
 	/** */
 	public static String BranchSelectionDialog_Refs;
 
+	/** */
+	public static String Decorator_exceptionMessage;
+
+	/** */
+	public static String DecoratorPreferencesPage_addVariablesTitle;
+
+	/** */
+	public static String DecoratorPreferencesPage_addVariablesAction;
+
+	/** */
+	public static String DecoratorPreferencesPage_computeDeep;
+
+	/** */
+	public static String DecoratorPreferencesPage_description;
+
+	/** */
+	public static String DecoratorPreferencesPage_decorationSettings;
+
+	/** */
+	public static String DecoratorPreferencesPage_preview;
+
+	/** */
+	public static String DecoratorPreferencesPage_fileFormatLabel;
+
+	/** */
+	public static String DecoratorPreferencesPage_folderFormatLabel;
+
+	/** */
+	public static String DecoratorPreferencesPage_projectFormatLabel;
+
+	/** */
+	public static String DecoratorPreferencesPage_fileFormatDefault;
+
+	/** */
+	public static String DecoratorPreferencesPage_projectFormatDefault;
+
+	/** */
+	public static String DecoratorPreferencesPage_folderFormatDefault;
+
+	/** */
+	public static String DecoratorPreferencesPage_generalTabFolder;
+
+	/** */
+	public static String DecoratorPreferencesPage_nameResourceVariable;
+
+	/** */
+	public static String DecoratorPreferencesPage_selectFormats;
+
+	/** */
+	public static String DecoratorPreferencesPage_selectVariablesToAdd;
+
+	/** */
+	public static String DecoratorPreferencesPage_textLabel;
+
+	/** */
+	public static String DecoratorPreferencesPage_iconLabel;
+
+	/** */
+	public static String DecoratorPreferencesPage_labelDecorationsLink;
+
 	static {
 		initializeMessages(UIText.class.getPackage().getName() + ".uitext",
 				UIText.class);
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/BranchAction.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/BranchAction.java
index 7ca4d10..38ee3d8 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/BranchAction.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/BranchAction.java
@@ -19,7 +19,7 @@
 import org.eclipse.jface.operation.IRunnableWithProgress;
 import org.eclipse.swt.widgets.Display;
 import org.spearce.egit.core.op.BranchOperation;
-import org.spearce.egit.ui.internal.decorators.GitResourceDecorator;
+import org.spearce.egit.ui.internal.decorators.GitLightweightDecorator;
 import org.spearce.egit.ui.internal.dialogs.BranchSelectionDialog;
 import org.spearce.jgit.lib.Repository;
 
@@ -56,7 +56,7 @@ public void run(final IProgressMonitor monitor)
 				throws InvocationTargetException {
 					try {
 						new BranchOperation(repository, refName).run(monitor);
-						GitResourceDecorator.refresh();
+						GitLightweightDecorator.refresh();
 					} catch (final CoreException ce) {
 						ce.printStackTrace();
 						Display.getDefault().asyncExec(new Runnable() {
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/Disconnect.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/Disconnect.java
index 18d6b4b..4201822 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/Disconnect.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/Disconnect.java
@@ -13,7 +13,7 @@
 import org.eclipse.core.resources.IWorkspaceRunnable;
 import org.eclipse.jface.action.IAction;
 import org.spearce.egit.core.op.DisconnectProviderOperation;
-import org.spearce.egit.ui.internal.decorators.GitResourceDecorator;
+import org.spearce.egit.ui.internal.decorators.GitLightweightDecorator;
 
 /**
  *	Action to disassociate a project from its Git repository.
@@ -27,6 +27,6 @@ protected IWorkspaceRunnable createOperation(final IAction act,
 	}
 
 	protected void postOperation() {
-		GitResourceDecorator.refresh();
+		GitLightweightDecorator.refresh();
 	}
 }
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/ResetAction.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/ResetAction.java
index b05cdd3..a329925 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/ResetAction.java
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/actions/ResetAction.java
@@ -19,7 +19,7 @@
 import org.eclipse.jface.operation.IRunnableWithProgress;
 import org.spearce.egit.core.op.ResetOperation;
 import org.spearce.egit.core.op.ResetOperation.ResetType;
-import org.spearce.egit.ui.internal.decorators.GitResourceDecorator;
+import org.spearce.egit.ui.internal.decorators.GitLightweightDecorator;
 import org.spearce.egit.ui.internal.dialogs.BranchSelectionDialog;
 import org.spearce.jgit.lib.Repository;
 
@@ -55,7 +55,7 @@ public void run(final IProgressMonitor monitor)
 					throws InvocationTargetException {
 						try {
 							new ResetOperation(repository, refName, type).run(monitor);
-							GitResourceDecorator.refresh();
+							GitLightweightDecorator.refresh();
 						} catch (CoreException ce) {
 							ce.printStackTrace();
 							throw new InvocationTargetException(ce);
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java
new file mode 100644
index 0000000..85b9173
--- /dev/null
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitLightweightDecorator.java
@@ -0,0 +1,538 @@
+/*******************************************************************************
+ * Copyright (C) 2007, IBM Corporation and others
+ * Copyright (C) 2007, Dave Watson <[email-masked]>
+ * Copyright (C) 2008, Robin Rosenberg <[email-masked]>
+ * Copyright (C) 2008, Shawn O. Pearce <[email-masked]>
+ * Copyright (C) 2008, Google Inc.
+ * Copyright (C) 2008, Tor Arne Vestbø <[email-masked]>
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * See LICENSE for the full license text, also available.
+ *******************************************************************************/
+
+package org.spearce.egit.ui.internal.decorators;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.resources.mapping.ResourceMapping;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.jface.viewers.IDecoration;
+import org.eclipse.jface.viewers.ILightweightLabelDecorator;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.LabelProviderChangedEvent;
+import org.eclipse.osgi.util.TextProcessor;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.team.ui.TeamUI;
+import org.eclipse.ui.IContributorResourceAdapter;
+import org.eclipse.ui.PlatformUI;
+import org.spearce.egit.core.internal.util.ExceptionCollector;
+import org.spearce.egit.core.project.GitProjectData;
+import org.spearce.egit.core.project.RepositoryChangeListener;
+import org.spearce.egit.core.project.RepositoryMapping;
+import org.spearce.egit.ui.Activator;
+import org.spearce.egit.ui.UIPreferences;
+import org.spearce.egit.ui.UIText;
+import org.spearce.jgit.lib.IndexChangedEvent;
+import org.spearce.jgit.lib.RefsChangedEvent;
+import org.spearce.jgit.lib.Repository;
+import org.spearce.jgit.lib.RepositoryChangedEvent;
+import org.spearce.jgit.lib.RepositoryListener;
+
+/**
+ * Supplies annotations for displayed resources
+ * 
+ * This decorator provides annotations to indicate the status of each resource
+ * when compared to <code>HEAD</code>, as well as the index in the relevant
+ * repository.
+ * 
+ * TODO: Add support for colors and font decoration
+ */
+public class GitLightweightDecorator extends LabelProvider implements
+		ILightweightLabelDecorator, IPropertyChangeListener,
+		IResourceChangeListener, RepositoryChangeListener, RepositoryListener {
+
+	/**
+	 * Property constant pointing back to the extension point id of the
+	 * decorator
+	 */
+	public static final String DECORATOR_ID = "org.spearce.egit.ui.internal.decorators.GitLightweightDecorator"; //$NON-NLS-1$
+
+	/**
+	 * Bit-mask describing interesting changes for IResourceChangeListener
+	 * events
+	 */
+	private static int INTERESTING_CHANGES = IResourceDelta.CONTENT
+			| IResourceDelta.MOVED_FROM | IResourceDelta.MOVED_TO
+			| IResourceDelta.OPEN | IResourceDelta.REPLACED
+			| IResourceDelta.TYPE;
+
+	/**
+	 * Collector for keeping the error view from filling up with exceptions
+	 */
+	private static ExceptionCollector exceptions = new ExceptionCollector(
+			UIText.Decorator_exceptionMessage, Activator.getPluginId(),
+			IStatus.ERROR, Activator.getDefault().getLog());
+
+	/**
+	 * Constructs a new Git resource decorator
+	 */
+	public GitLightweightDecorator() {
+		TeamUI.addPropertyChangeListener(this);
+		Activator.addPropertyChangeListener(this);
+		PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
+				.addPropertyChangeListener(this);
+		Repository.addAnyRepositoryChangedListener(this);
+		GitProjectData.addRepositoryChangeListener(this);
+		ResourcesPlugin.getWorkspace().addResourceChangeListener(this,
+				IResourceChangeEvent.POST_CHANGE);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
+	 */
+	@Override
+	public void dispose() {
+		super.dispose();
+		PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
+				.removePropertyChangeListener(this);
+		TeamUI.removePropertyChangeListener(this);
+		Activator.removePropertyChangeListener(this);
+		Repository.removeAnyRepositoryChangedListener(this);
+		GitProjectData.removeRepositoryChangeListener(this);
+		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
+	}
+
+	/**
+	 * This method should only be called by the decorator thread.
+	 * 
+	 * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang.Object,
+	 *      org.eclipse.jface.viewers.IDecoration)
+	 */
+	public void decorate(Object element, IDecoration decoration) {
+		final IResource resource = getResource(element);
+		if (resource == null)
+			return;
+
+		// Don't decorate the workspace root
+		if (resource.getType() == IResource.ROOT)
+			return;
+
+		// Don't decorate non-existing resources
+		if (!resource.exists() && !resource.isPhantom())
+			return;
+
+		// Make sure we're dealing with a Git project
+		final RepositoryMapping mapping = RepositoryMapping
+				.getMapping(resource);
+		if (mapping == null)
+			return;
+
+		// Cannot decorate linked resources
+		if (mapping.getRepoRelativePath(resource) == null)
+			return;
+
+		// Don't decorate if UI plugin is not running
+		Activator activator = Activator.getDefault();
+		if (activator == null)
+			return;
+
+		DecorationHelper helper = new DecorationHelper(activator
+				.getPreferenceStore());
+		helper.decorate(decoration, new DecoratableResourceAdapter(resource));
+	}
+
+	private class DecoratableResourceAdapter implements IDecoratableResource {
+
+		private IResource resource;
+
+		public DecoratableResourceAdapter(IResource resourceToWrap) {
+			resource = resourceToWrap;
+		}
+
+		public String getName() {
+			return resource.getName();
+		}
+
+		public int getType() {
+			return resource.getType();
+		}
+	}
+
+	/**
+	 * Helper class for doing resource decoration, based on the given
+	 * preferences
+	 * 
+	 * Used for real-time decoration, as well as in the decorator preview
+	 * preferences page
+	 */
+	public static class DecorationHelper {
+
+		private IPreferenceStore store;
+
+		/** */
+		public static final String BINDING_RESOURCE_NAME = "name"; //$NON-NLS-1$
+
+		/**
+		 * Constructs a decorator using the rules from the given
+		 * <code>preferencesStore</code>
+		 * 
+		 * @param preferencesStore
+		 *            the preferences store with the preferred decorator rules
+		 */
+		public DecorationHelper(IPreferenceStore preferencesStore) {
+			store = preferencesStore;
+		}
+
+		/**
+		 * Decorates the given <code>decoration</code> based on the state of the
+		 * given <code>resource</code>, using the preferences passed when
+		 * constructing this decoration helper.
+		 * 
+		 * @param decoration
+		 *            the decoration to decorate
+		 * @param resource
+		 *            the resource to retrieve state from
+		 */
+		public void decorate(IDecoration decoration,
+				IDecoratableResource resource) {
+			String format = "";
+			switch (resource.getType()) {
+			case IResource.FILE:
+				format = store
+						.getString(UIPreferences.DECORATOR_FILETEXT_DECORATION);
+				break;
+			case IResource.FOLDER:
+				format = store
+						.getString(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION);
+				break;
+			case IResource.PROJECT:
+				format = store
+						.getString(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION);
+				break;
+			}
+
+			Map<String, String> bindings = new HashMap<String, String>();
+			bindings.put(BINDING_RESOURCE_NAME, resource.getName());
+
+			decorate(decoration, format, bindings);
+		}
+
+		/**
+		 * Decorates the given <code>decoration</code>, using the given
+		 * <code>format</code>, and mapped using <code>bindings</code>
+		 * 
+		 * @param decoration
+		 *            the decoration to decorate
+		 * @param format
+		 *            the format to base the decoration on
+		 * @param bindings
+		 *            the bindings between variables in the format and actual
+		 *            values
+		 */
+		public static void decorate(IDecoration decoration, String format,
+				Map bindings) {
+			StringBuffer prefix = new StringBuffer();
+			StringBuffer suffix = new StringBuffer();
+			StringBuffer output = prefix;
+
+			int length = format.length();
+			int start = -1;
+			int end = length;
+			while (true) {
+				if ((end = format.indexOf('{', start)) > -1) {
+					output.append(format.substring(start + 1, end));
+					if ((start = format.indexOf('}', end)) > -1) {
+						String key = format.substring(end + 1, start);
+						String s;
+
+						// We use the BINDING_RESOURCE_NAME key to determine if
+						// we are doing the prefix or suffix. The name isn't
+						// actually part of either.
+						if (key.equals(BINDING_RESOURCE_NAME)) {
+							output = suffix;
+							s = null;
+						} else {
+							s = (String) bindings.get(key);
+						}
+
+						if (s != null) {
+							output.append(s);
+						} else {
+							// Support removing prefix character if binding is
+							// null
+							int curLength = output.length();
+							if (curLength > 0) {
+								char c = output.charAt(curLength - 1);
+								if (c == ':' || c == '@') {
+									output.deleteCharAt(curLength - 1);
+								}
+							}
+						}
+					} else {
+						output.append(format.substring(end, length));
+						break;
+					}
+				} else {
+					output.append(format.substring(start + 1, length));
+					break;
+				}
+			}
+
+			String prefixString = prefix.toString().replaceAll("^\\s+", "");
+			if (prefixString != null) {
+				decoration.addPrefix(TextProcessor.process(prefixString,
+						"()[].")); //$NON-NLS-1$
+			}
+			String suffixString = suffix.toString().replaceAll("\\s+$", "");
+			if (suffixString != null) {
+				decoration.addSuffix(TextProcessor.process(suffixString,
+						"()[].")); //$NON-NLS-1$
+			}
+		}
+	}
+
+	// -------- Refresh handling --------
+
+	/**
+	 * Perform a blanket refresh of all decorations
+	 */
+	public static void refresh() {
+		Display.getDefault().asyncExec(new Runnable() {
+			public void run() {
+				Activator.getDefault().getWorkbench().getDecoratorManager()
+						.update(DECORATOR_ID);
+			}
+		});
+	}
+
+	/**
+	 * Callback for IPropertyChangeListener events
+	 * 
+	 * If any of the relevant preferences has been changed we refresh all
+	 * decorations (all projects and their resources).
+	 * 
+	 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
+	 */
+	public void propertyChange(PropertyChangeEvent event) {
+		final String prop = event.getProperty();
+		// If the property is of any interest to us
+		if (prop.equals(TeamUI.GLOBAL_IGNORES_CHANGED)
+				|| prop.equals(TeamUI.GLOBAL_FILE_TYPES_CHANGED)
+				|| prop.equals(Activator.DECORATORS_CHANGED)) {
+			postLabelEvent(new LabelProviderChangedEvent(this, null /* all */));
+		}
+	}
+
+	/**
+	 * Callback for IResourceChangeListener events
+	 * 
+	 * Schedules a refresh of the changed resource
+	 * 
+	 * If the preference for computing deep dirty states has been set we walk
+	 * the ancestor tree of the changed resource and update all parents as well.
+	 * 
+	 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
+	 */
+	public void resourceChanged(IResourceChangeEvent event) {
+		final Set<IResource> resourcesToUpdate = new HashSet<IResource>();
+
+		try { // Compute the changed resources by looking at the delta
+			event.getDelta().accept(new IResourceDeltaVisitor() {
+				public boolean visit(IResourceDelta delta) throws CoreException {
+					final IResource resource = delta.getResource();
+
+					if (resource.getType() == IResource.ROOT) {
+						// Continue with the delta
+						return true;
+					}
+
+					if (resource.getType() == IResource.PROJECT) {
+						// If the project is not accessible, don't process it
+						if (!resource.isAccessible())
+							return false;
+					}
+
+					// If the file has changed but not in a way that we care
+					// about
+					// (e.g. marker changes to files) then ignore the change
+					if (delta.getKind() == IResourceDelta.CHANGED
+							&& (delta.getFlags() & INTERESTING_CHANGES) == 0) {
+						return true;
+					}
+
+					// All seems good, schedule the resource for update
+					resourcesToUpdate.add(resource);
+					return true;
+				}
+			}, true /* includePhantoms */);
+		} catch (final CoreException e) {
+			handleException(null, e);
+		}
+
+		// If deep decorator calculation is enabled in the preferences we
+		// walk the ancestor tree of each of the changed resources and add
+		// their parents to the update set
+		final IPreferenceStore store = Activator.getDefault()
+				.getPreferenceStore();
+		if (store.getBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY)) {
+			final IResource[] changedResources = resourcesToUpdate
+					.toArray(new IResource[resourcesToUpdate.size()]);
+			for (int i = 0; i < changedResources.length; i++) {
+				IResource current = changedResources[i];
+				while (current.getType() != IResource.ROOT) {
+					current = current.getParent();
+					resourcesToUpdate.add(current);
+				}
+			}
+		}
+
+		postLabelEvent(new LabelProviderChangedEvent(this, resourcesToUpdate
+				.toArray()));
+	}
+
+	/**
+	 * Callback for RepositoryListener events
+	 * 
+	 * We resolve the repository mapping for the changed repository and forward
+	 * that to repositoryChanged(RepositoryMapping).
+	 * 
+	 * @param e
+	 *            The original change event
+	 */
+	private void repositoryChanged(RepositoryChangedEvent e) {
+		final Set<RepositoryMapping> ms = new HashSet<RepositoryMapping>();
+		for (final IProject p : ResourcesPlugin.getWorkspace().getRoot()
+				.getProjects()) {
+			final RepositoryMapping mapping = RepositoryMapping.getMapping(p);
+			if (mapping != null && mapping.getRepository() == e.getRepository())
+				ms.add(mapping);
+		}
+		for (final RepositoryMapping m : ms) {
+			repositoryChanged(m);
+		}
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see
+	 * org.spearce.jgit.lib.RepositoryListener#indexChanged(org.spearce.jgit
+	 * .lib.IndexChangedEvent)
+	 */
+	public void indexChanged(IndexChangedEvent e) {
+		repositoryChanged(e);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see
+	 * org.spearce.jgit.lib.RepositoryListener#refsChanged(org.spearce.jgit.
+	 * lib.RefsChangedEvent)
+	 */
+	public void refsChanged(RefsChangedEvent e) {
+		repositoryChanged(e);
+	}
+
+	/**
+	 * Callback for RepositoryChangeListener events, as well as
+	 * RepositoryListener events via repositoryChanged()
+	 * 
+	 * We resolve the project and schedule a refresh of each resource in the
+	 * project.
+	 * 
+	 * @see org.spearce.egit.core.project.RepositoryChangeListener#repositoryChanged(org.spearce.egit.core.project.RepositoryMapping)
+	 */
+	public void repositoryChanged(RepositoryMapping mapping) {
+		final IProject project = mapping.getContainer().getProject();
+		if (project == null)
+			return;
+
+		final List<IResource> resources = new ArrayList<IResource>();
+		try {
+			project.accept(new IResourceVisitor() {
+				public boolean visit(IResource resource) {
+					resources.add(resource);
+					return true;
+				}
+			});
+			postLabelEvent(new LabelProviderChangedEvent(this, resources
+					.toArray()));
+		} catch (final CoreException e) {
+			handleException(project, e);
+		}
+	}
+
+	// -------- Helper methods --------
+
+	private static IResource getResource(Object element) {
+		if (element instanceof ResourceMapping) {
+			element = ((ResourceMapping) element).getModelObject();
+		}
+
+		IResource resource = null;
+		if (element instanceof IResource) {
+			resource = (IResource) element;
+		} else if (element instanceof IAdaptable) {
+			final IAdaptable adaptable = (IAdaptable) element;
+			resource = (IResource) adaptable.getAdapter(IResource.class);
+			if (resource == null) {
+				final IContributorResourceAdapter adapter = (IContributorResourceAdapter) adaptable
+						.getAdapter(IContributorResourceAdapter.class);
+				if (adapter != null)
+					resource = adapter.getAdaptedResource(adaptable);
+			}
+		}
+
+		return resource;
+	}
+
+	/**
+	 * Post the label event to the UI thread
+	 * 
+	 * @param event
+	 *            The event to post
+	 */
+	private void postLabelEvent(final LabelProviderChangedEvent event) {
+		Display.getDefault().asyncExec(new Runnable() {
+			public void run() {
+				fireLabelProviderChanged(event);
+			}
+		});
+	}
+
+	/**
+	 * Handle exceptions that occur in the decorator. Exceptions are only logged
+	 * for resources that are accessible (i.e. exist in an open project).
+	 * 
+	 * @param resource
+	 *            The resource that triggered the exception
+	 * @param e
+	 *            The exception that occurred
+	 */
+	private static void handleException(IResource resource, CoreException e) {
+		if (resource == null || resource.isAccessible())
+			exceptions.handleException(e);
+	}
+}
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitResourceDecorator.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitResourceDecorator.java
deleted file mode 100644
index f24b1eb..0000000
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/GitResourceDecorator.java
+++ /dev/null
@@ -1,454 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2007, Dave Watson <[email-masked]>
- * Copyright (C) 2008, Robin Rosenberg <[email-masked]>
- * Copyright (C) 2008, Shawn O. Pearce <[email-masked]>
- * Copyright (C) 2008, Google Inc.
- *
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * See LICENSE for the full license text, also available.
- *******************************************************************************/
-package org.spearce.egit.ui.internal.decorators;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
-import org.eclipse.core.resources.IContainer;
-import org.eclipse.core.resources.IFile;
-import org.eclipse.core.resources.IProject;
-import org.eclipse.core.resources.IResource;
-import org.eclipse.core.resources.IResourceChangeEvent;
-import org.eclipse.core.resources.IResourceChangeListener;
-import org.eclipse.core.resources.IResourceDelta;
-import org.eclipse.core.resources.IResourceDeltaVisitor;
-import org.eclipse.core.resources.IResourceVisitor;
-import org.eclipse.core.resources.ResourcesPlugin;
-import org.eclipse.core.runtime.CoreException;
-import org.eclipse.core.runtime.IAdaptable;
-import org.eclipse.core.runtime.IProgressMonitor;
-import org.eclipse.core.runtime.IStatus;
-import org.eclipse.core.runtime.QualifiedName;
-import org.eclipse.core.runtime.Status;
-import org.eclipse.core.runtime.jobs.ISchedulingRule;
-import org.eclipse.core.runtime.jobs.Job;
-import org.eclipse.jface.viewers.IDecoration;
-import org.eclipse.jface.viewers.ILightweightLabelDecorator;
-import org.eclipse.jface.viewers.LabelProvider;
-import org.eclipse.jface.viewers.LabelProviderChangedEvent;
-import org.eclipse.swt.widgets.Display;
-import org.eclipse.team.core.Team;
-import org.eclipse.ui.IDecoratorManager;
-import org.spearce.egit.core.project.GitProjectData;
-import org.spearce.egit.core.project.RepositoryChangeListener;
-import org.spearce.egit.core.project.RepositoryMapping;
-import org.spearce.egit.ui.Activator;
-import org.spearce.egit.ui.UIIcons;
-import org.spearce.egit.ui.UIText;
-import org.spearce.jgit.lib.Constants;
-import org.spearce.jgit.lib.GitIndex;
-import org.spearce.jgit.lib.IndexChangedEvent;
-import org.spearce.jgit.lib.RefsChangedEvent;
-import org.spearce.jgit.lib.Repository;
-import org.spearce.jgit.lib.RepositoryChangedEvent;
-import org.spearce.jgit.lib.RepositoryListener;
-import org.spearce.jgit.lib.RepositoryState;
-import org.spearce.jgit.lib.Tree;
-import org.spearce.jgit.lib.TreeEntry;
-import org.spearce.jgit.lib.GitIndex.Entry;
-
-/**
- * Supplies annotations for displayed resources.
- * <p>
- * This decorator provides annotations to indicate the status of each resource
- * when compared to <code>HEAD</code> as well as the index in the relevant
- * repository.
- * 
- * When either the index or the working directory is different from HEAD an
- * indicator is set.
- * 
- * </p>
- */
-public class GitResourceDecorator extends LabelProvider implements
-		ILightweightLabelDecorator {
-
-	static final String decoratorId = "org.spearce.egit.ui.internal.decorators.GitResourceDecorator";
-	static class ResCL extends Job implements IResourceChangeListener, RepositoryChangeListener, RepositoryListener {
-
-		ResCL() {
-			super("Git resource decorator trigger");
-		}
-
-		GitResourceDecorator getActiveDecorator() {
-			IDecoratorManager decoratorManager = Activator.getDefault()
-					.getWorkbench().getDecoratorManager();
-			if (decoratorManager.getEnabled(decoratorId))
-				return (GitResourceDecorator) decoratorManager
-						.getLightweightLabelDecorator(decoratorId);
-			return null;
-		}
-
-		private Set<IResource> resources = new LinkedHashSet<IResource>();
-
-		public void refsChanged(RefsChangedEvent e) {
-			repositoryChanged(e);
-		}
-
-		public void indexChanged(IndexChangedEvent e) {
-			repositoryChanged(e);
-		}
-
-		private void repositoryChanged(RepositoryChangedEvent e) {
-			Set<RepositoryMapping> ms = new HashSet<RepositoryMapping>();
-			for (IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
-				RepositoryMapping mapping = RepositoryMapping.getMapping(p);
-				if (mapping != null && mapping.getRepository() == e.getRepository())
-					ms.add(mapping);
-			}
-			for (RepositoryMapping m : ms) {
-				repositoryChanged(m);
-			}
-		}
-
-		public void repositoryChanged(final RepositoryMapping which) {
-			synchronized (resources) {
-				resources.add(which.getContainer());
-			}
-			schedule();
-		}
-
-		@Override
-		protected IStatus run(IProgressMonitor arg0) {
-			try {
-				if (resources.size() > 0) {
-					IResource m;
-					synchronized(resources) {
-						Iterator<IResource> i = resources.iterator();
-						m = i.next();
-						i.remove();
-
-						while (!m.isAccessible()) {
-							if (!i.hasNext())
-								return Status.OK_STATUS;
-							m = i.next();
-							i.remove();
-						}
-
-						if (resources.size() > 0)
-							schedule();
-					}
-					ISchedulingRule markerRule = m.getWorkspace().getRuleFactory().markerRule(m);
-					getJobManager().beginRule(markerRule, arg0);
-					try {
-						m.accept(new IResourceVisitor() {
-							public boolean visit(IResource resource) throws CoreException {
-								GitResourceDecorator decorator = getActiveDecorator();
-								if (decorator != null)
-									decorator.clearDecorationState(resource);
-								return true;
-							}
-						},
-						IResource.DEPTH_INFINITE,
-						true);
-					} finally {
-						getJobManager().endRule(markerRule);
-					}
-				}
-				return Status.OK_STATUS;
-			} catch (Exception e) {
-				// We must be silent here or the UI will panic with lots of error messages
-				Activator.logError("Failed to trigger resource re-decoration", e);
-				return Status.OK_STATUS;
-			}
-		}
-
-		public void resourceChanged(IResourceChangeEvent event) {
-			if (event.getType() != IResourceChangeEvent.POST_CHANGE) {
-				return;
-			}
-			try {
-				event.getDelta().accept(new IResourceDeltaVisitor() {
-					public boolean visit(IResourceDelta delta)
-							throws CoreException {
-						for (IResource r = delta.getResource(); r.getType() != IResource.ROOT; r = r
-								.getParent()) {
-							synchronized (resources) {
-								resources.add(r);
-							}
-						}
-						return true;
-					}
-				},
-				true
-				);
-			} catch (Exception e) {
-				Activator.logError("Problem during decorations. Stopped", e);
-			}
-			schedule();
-		}
-
-		void force() {
-			for (IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
-				synchronized (resources) {
-					resources.add(p);
-				}
-			}
-			schedule();
-		}
-	} // End ResCL
-
-	void clearDecorationState(IResource r) throws CoreException {
-		if (r.isAccessible()) {
-			r.setSessionProperty(GITFOLDERDIRTYSTATEPROPERTY, null);
-			fireLabelProviderChanged(new LabelProviderChangedEvent(this, r));
-		}
-	}
-
-	static ResCL myrescl = new ResCL();
-
-	static {
-		Repository.addAnyRepositoryChangedListener(myrescl);
-		GitProjectData.addRepositoryChangeListener(myrescl);
-		ResourcesPlugin.getWorkspace().addResourceChangeListener(myrescl,
-				IResourceChangeEvent.POST_CHANGE);
-	}
-
-	/**
-	 * Request that the decorator be updated, to reflect any recent changes.
-	 * <p>
-	 * Can be invoked any any thread. If the current thread is not the UI
-	 * thread, an async update will be scheduled.
-	 * </p>
-	 */
-	public static void refresh() {
-		myrescl.force();
-	}
-
-	private static IResource toIResource(final Object e) {
-		if (e instanceof IResource)
-			return (IResource) e;
-		if (e instanceof IAdaptable) {
-			final Object c = ((IAdaptable) e).getAdapter(IResource.class);
-			if (c instanceof IResource)
-				return (IResource) c;
-		}
-		return null;
-	}
-
-	static QualifiedName GITFOLDERDIRTYSTATEPROPERTY = new QualifiedName(
-			"org.spearce.egit.ui.internal.decorators.GitResourceDecorator",
-			"dirty");
-
-	static final int UNCHANGED = 0;
-
-	static final int CHANGED = 1;
-
-	private Boolean isDirty(IResource rsrc) {
-		try {
-			if (rsrc.getType() == IResource.FILE && Team.isIgnored((IFile)rsrc))
-				return Boolean.FALSE;
-
-			RepositoryMapping mapped = RepositoryMapping.getMapping(rsrc);
-			if (mapped != null) {
-				if (rsrc instanceof IContainer) {
-					for (IResource r : ((IContainer) rsrc)
-							.members(IContainer.EXCLUDE_DERIVED)) {
-						Boolean f = isDirty(r);
-						if (f == null || f.booleanValue())
-							return Boolean.TRUE;
-					}
-					return Boolean.FALSE;
-				}
-
-				return Boolean.valueOf(mapped.isResourceChanged(rsrc));
-			}
-			return null; // not mapped
-		} catch (CoreException e) {
-			// TODO Auto-generated catch block
-			e.printStackTrace();
-		} catch (IOException e) {
-			// TODO Auto-generated catch block
-			e.printStackTrace();
-		}
-		return null;
-	}
-
-	public void decorate(final Object element, final IDecoration decoration) {
-		final IResource rsrc = toIResource(element);
-		if (rsrc == null)
-			return;
-
-		// If the workspace has not been refreshed properly a resource might
-		// not actually exist, so we ignore these and do not decorate them
-		if (!rsrc.exists() && !rsrc.isPhantom()) {
-			Activator.trace("Tried to decorate non-existent resource "+rsrc);
-			return;
-		}
-
-		RepositoryMapping mapped = RepositoryMapping.getMapping(rsrc);
-
-		// TODO: How do I see a renamed resource?
-		// TODO: Even trickier: when a path change from being blob to tree?
-		try {
-			if (mapped != null) {
-				Repository repository = mapped.getRepository();
-				GitIndex index = repository.getIndex();
-				String repoRelativePath = mapped.getRepoRelativePath(rsrc);
-
-				if (repoRelativePath == null) {
-					Activator.trace("Cannot decorate linked resource " + rsrc);
-					return;
-				}
-
-				Tree headTree = repository.mapTree(Constants.HEAD);
-				TreeEntry blob = headTree!=null ? headTree.findBlobMember(repoRelativePath) : null;
-				Entry entry = index.getEntry(repoRelativePath);
-				if (entry == null) {
-					if (blob == null) {
-						if (rsrc instanceof IContainer) {
-							Integer df = (Integer) rsrc
-									.getSessionProperty(GITFOLDERDIRTYSTATEPROPERTY);
-							Boolean f = df == null ? isDirty(rsrc)
-									: Boolean.valueOf(df.intValue() == CHANGED);
-							if (f != null) {
-								if (f.booleanValue()) {
-									decoration.addPrefix(">"); // Have not
-									// seen
-									orState(rsrc, CHANGED);
-								} else {
-									orState(rsrc, UNCHANGED);
-									// decoration.addSuffix("=?");
-								}
-							} else {
-								decoration.addSuffix(" ?* ");
-							}
-
-							if (rsrc instanceof IProject) {
-								Repository repo = mapped.getRepository();
-								try {
-									String branch = repo.getBranch();
-									RepositoryState repositoryState = repo.getRepositoryState();
-									String statename;
-									if (repositoryState.equals(RepositoryState.SAFE))
-										statename = "";
-									else
-										statename = repositoryState.getDescription() + " ";
-									decoration.addSuffix(" [Git " + statename + "@ " + branch + "]");
-								} catch (IOException e) {
-									e.printStackTrace();
-									decoration.addSuffix(" [Git ?]");
-								}
-								decoration.addOverlay(UIIcons.OVR_SHARED);
-							}
-
-						} else {
-							if (Team.isIgnoredHint(rsrc)) {
-								decoration.addSuffix("(ignored)");
-							} else {
-								decoration.addPrefix(">");
-								decoration.addSuffix("(untracked)");
-								orState(rsrc.getParent(), CHANGED);
-							}
-						}
-					} else {
-						if (!(rsrc instanceof IContainer)) {
-							decoration.addSuffix("(deprecated)"); // Will drop on
-							// commit
-							decoration.addOverlay(UIIcons.OVR_PENDING_REMOVE);
-							orState(rsrc.getParent(), CHANGED);
-						}
-					}
-				} else {
-					if (entry.getStage() != GitIndex.STAGE_0) {
-						decoration.addSuffix("(conflict)");
-						decoration.addOverlay(UIIcons.OVR_CONFLICT);
-						orState(rsrc.getParent(), CHANGED);
-						return;
-					}
-
-					if (blob == null) {
-						decoration.addOverlay(UIIcons.OVR_PENDING_ADD);
-						orState(rsrc.getParent(), CHANGED);
-					} else {
-
-						if (entry.isAssumedValid()) {
-							decoration.addOverlay(UIIcons.OVR_ASSUMEVALID);
-							return;
-						}
-
-						decoration.addOverlay(UIIcons.OVR_SHARED);
-
-						if (entry.isModified(mapped.getWorkDir(), true)) {
-							decoration.addPrefix(">");
-							decoration.addSuffix("(not updated)");
-							orState(rsrc.getParent(), CHANGED);
-						} else {
-							if (!entry.getObjectId().equals(blob.getId()))
-								decoration.addPrefix(">");
-							else
-								decoration.addPrefix(""); // set it to avoid further calls
-						}
-					}
-				}
-			}
-		} catch (IOException e) {
-			decoration.addSuffix("?");
-			// If we throw an exception Eclipse will log the error and
-			// unregister us thereby preventing us from dragging down the
-			// entire workbench because we are crashing.
-			//
-			throw new RuntimeException(UIText.Decorator_failedLazyLoading, e);
-		} catch (CoreException e) {
-			throw new RuntimeException(UIText.Decorator_failedLazyLoading, e);
-		}
-	}
-
-	private void orState(final IResource rsrc, int flag) {
-		if (rsrc == null || rsrc.getType() == IResource.ROOT) {
-			return;
-		}
-
-		try {
-			Integer dirty = (Integer) rsrc.getSessionProperty(GITFOLDERDIRTYSTATEPROPERTY);
-			Runnable runnable = new Runnable() {
-				public void run() {
-					// Async could be called after a
-					// project is closed or a
-					// resource is deleted
-					if (!rsrc.isAccessible())
-						return;
-					fireLabelProviderChanged(new LabelProviderChangedEvent(
-							GitResourceDecorator.this, rsrc));
-				}
-			};
-			if (dirty == null) {
-				rsrc.setSessionProperty(GITFOLDERDIRTYSTATEPROPERTY, new Integer(flag));
-				orState(rsrc.getParent(), flag);
-//				if (Thread.currentThread() == Display.getDefault().getThread())
-//					runnable.run();
-//				else
-					Display.getDefault().asyncExec(runnable);
-			} else {
-				if ((dirty.intValue() | flag) != dirty.intValue()) {
-					dirty = new Integer(dirty.intValue() | flag);
-					rsrc.setSessionProperty(GITFOLDERDIRTYSTATEPROPERTY, dirty);
-					orState(rsrc.getParent(), dirty.intValue());
-//					if (Thread.currentThread() == Display.getDefault().getThread())
-//						runnable.run();
-//					else
-						Display.getDefault().asyncExec(runnable);
-				}
-			}
-		} catch (CoreException e) {
-			// TODO Auto-generated catch block
-			e.printStackTrace();
-		}
-	}
-	
-	@Override
-	public boolean isLabelProperty(Object element, String property) {
-		return super.isLabelProperty(element, property);
-	}
-}
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java
new file mode 100644
index 0000000..8d6c741
--- /dev/null
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/decorators/IDecoratableResource.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (C) 2008, Tor Arne Vestbø <[email-masked]>
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * See LICENSE for the full license text, also available.
+ *******************************************************************************/
+
+package org.spearce.egit.ui.internal.decorators;
+
+import org.eclipse.core.resources.IResource;
+
+/**
+ * Represents the state of a resource that can be used as a basis for decoration
+ */
+public interface IDecoratableResource {
+
+	/**
+	 * Gets the type of the resource as defined by {@link IResource}
+	 * 
+	 * @return the type of the resource
+	 */
+	int getType();
+
+	/**
+	 * Gets the name of the resource
+	 * 
+	 * @return the name of the resource
+	 */
+	String getName();
+}
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java
new file mode 100644
index 0000000..2ef0292
--- /dev/null
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/internal/preferences/GitDecoratorPreferencePage.java
@@ -0,0 +1,735 @@
+/*******************************************************************************
+ * Copyright (C) 2003, 2006 Subclipse project and others.
+ * Copyright (C) 2008, Tor Arne Vestbø <[email-masked]>
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * See LICENSE for the full license text, also available.
+ *******************************************************************************/
+package org.spearce.egit.ui.internal.preferences;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Observable;
+import java.util.Observer;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.preference.PreferencePage;
+import org.eclipse.jface.preference.PreferenceStore;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.resource.ResourceManager;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.jface.viewers.DecorationContext;
+import org.eclipse.jface.viewers.DecorationOverlayIcon;
+import org.eclipse.jface.viewers.IDecoration;
+import org.eclipse.jface.viewers.IDecorationContext;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.TabFolder;
+import org.eclipse.swt.widgets.TabItem;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.ListSelectionDialog;
+import org.eclipse.ui.ide.IDE.SharedImages;
+import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;
+import org.spearce.egit.ui.Activator;
+import org.spearce.egit.ui.UIPreferences;
+import org.spearce.egit.ui.UIText;
+import org.spearce.egit.ui.internal.SWTUtils;
+import org.spearce.egit.ui.internal.decorators.GitLightweightDecorator.DecorationHelper;
+import org.spearce.egit.ui.internal.decorators.IDecoratableResource;
+
+/**
+ * Preference page for customizing Git label decorations
+ */
+public class GitDecoratorPreferencePage extends PreferencePage implements
+		IWorkbenchPreferencePage {
+
+	private Text fileTextFormat;
+
+	private Text folderTextFormat;
+
+	private Text projectTextFormat;
+
+	private Button showDirty;
+
+	private Preview fPreview;
+
+	private static final Collection PREVIEW_FILESYSTEM_ROOT;
+
+	private static ThemeListener fThemeListener;
+
+	static {
+		final PreviewResource project = new PreviewResource(
+				"Project", IResource.PROJECT); //$NON-NLS-1$1
+		final ArrayList<PreviewResource> children = new ArrayList<PreviewResource>();
+		children.add(new PreviewResource("folder", IResource.FOLDER)); //$NON-NLS-1$
+		children.add(new PreviewResource("file.txt", IResource.FILE)); //$NON-NLS-1$
+		project.children = children;
+		PREVIEW_FILESYSTEM_ROOT = Collections.singleton(project);
+	}
+
+	/**
+	 * Constructs a decorator preference page
+	 */
+	public GitDecoratorPreferencePage() {
+		setDescription(UIText.DecoratorPreferencesPage_description);
+	}
+
+	/**
+	 * @see PreferencePage#createContents(Composite)
+	 */
+	protected Control createContents(Composite parent) {
+		Composite headerGroup = new Composite(parent, SWT.NULL);
+		GridLayout layout = new GridLayout();
+		layout.marginWidth = 0;
+		layout.marginHeight = 0;
+		layout.verticalSpacing = 10;
+		headerGroup.setLayout(layout);
+		GridData data = new GridData();
+		data.horizontalAlignment = GridData.FILL;
+		headerGroup.setLayoutData(data);
+
+		SWTUtils.createPreferenceLink(
+				(IWorkbenchPreferenceContainer) getContainer(), headerGroup,
+				"org.eclipse.ui.preferencePages.Decorators",
+				UIText.DecoratorPreferencesPage_labelDecorationsLink); //$NON-NLS-1$
+
+		SWTUtils.createLabel(headerGroup,
+				UIText.DecoratorPreferencesPage_decorationSettings);
+
+		TabFolder tabFolder = new TabFolder(parent, SWT.NONE);
+		GridData tabData = SWTUtils.createHVFillGridData();
+		tabData.heightHint = 100;
+		tabFolder.setLayoutData(tabData);
+
+		TabItem tabItem = new TabItem(tabFolder, SWT.NONE);
+		tabItem.setText(UIText.DecoratorPreferencesPage_generalTabFolder);
+		tabItem.setControl(createGeneralDecoratorPage(tabFolder));
+
+		tabItem = new TabItem(tabFolder, SWT.NONE);
+		tabItem.setText(UIText.DecoratorPreferencesPage_textLabel);
+		tabItem.setControl(createTextDecoratorPage(tabFolder));
+
+		tabItem = new TabItem(tabFolder, SWT.NONE);
+		tabItem.setText(UIText.DecoratorPreferencesPage_iconLabel);
+		tabItem.setControl(createIconDecoratorPage(tabFolder));
+
+		initializeValues();
+
+		fPreview = new Preview(parent);
+		fPreview.refresh();
+
+		// TODO: Add help text for this preference page
+
+		fThemeListener = new ThemeListener(fPreview);
+		PlatformUI.getWorkbench().getThemeManager().addPropertyChangeListener(
+				fThemeListener);
+		Dialog.applyDialogFont(parent);
+		return tabFolder;
+	}
+
+	private Control createGeneralDecoratorPage(Composite parent) {
+		Composite composite = new Composite(parent, SWT.NULL);
+
+		GridLayout layout = new GridLayout();
+		composite.setLayout(layout);
+		GridData data = new GridData();
+		data.horizontalAlignment = GridData.FILL;
+		composite.setLayoutData(data);
+
+		showDirty = SWTUtils.createCheckBox(composite,
+				UIText.DecoratorPreferencesPage_computeDeep);
+
+		return composite;
+	}
+
+	/**
+	 * Creates the controls for the first tab folder
+	 * 
+	 * @param parent
+	 * 
+	 * @return the control
+	 */
+	private Control createTextDecoratorPage(Composite parent) {
+		Composite fileTextGroup = new Composite(parent, SWT.NULL);
+		GridLayout layout = new GridLayout();
+		layout.numColumns = 3;
+		fileTextGroup.setLayout(layout);
+		GridData data = new GridData();
+		data.horizontalAlignment = GridData.FILL;
+		fileTextGroup.setLayoutData(data);
+
+		TextPair format = createFormatEditorControl(fileTextGroup,
+				UIText.DecoratorPreferencesPage_fileFormatLabel,
+				UIText.DecoratorPreferencesPage_addVariablesAction,
+				getFileBindingDescriptions());
+		fileTextFormat = format.t1;
+
+		format = createFormatEditorControl(fileTextGroup,
+				UIText.DecoratorPreferencesPage_folderFormatLabel,
+				UIText.DecoratorPreferencesPage_addVariablesAction,
+				getFolderBindingDescriptions());
+		folderTextFormat = format.t1;
+
+		format = createFormatEditorControl(fileTextGroup,
+				UIText.DecoratorPreferencesPage_projectFormatLabel,
+				UIText.DecoratorPreferencesPage_addVariablesAction,
+				getProjectBindingDescriptions());
+		projectTextFormat = format.t1;
+
+		return fileTextGroup;
+	}
+
+	private Control createIconDecoratorPage(Composite parent) {
+		Composite imageGroup = new Composite(parent, SWT.NULL);
+		GridLayout layout = new GridLayout();
+		imageGroup.setLayout(layout);
+		GridData data = new GridData();
+		data.horizontalAlignment = GridData.FILL;
+		imageGroup.setLayoutData(data);
+
+		return imageGroup;
+	}
+
+	private TextPair createFormatEditorControl(Composite composite,
+			String title, String buttonText, final Map supportedBindings) {
+
+		SWTUtils.createLabel(composite, title);
+
+		Text format = new Text(composite, SWT.BORDER);
+		format.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+		format.addModifyListener(new ModifyListener() {
+			public void modifyText(ModifyEvent e) {
+				updatePreview();
+			}
+		});
+		Button b = new Button(composite, SWT.NONE);
+		b.setText(buttonText);
+		GridData data = new GridData();
+		data.horizontalAlignment = GridData.FILL;
+		int widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
+		data.widthHint = Math.max(widthHint, b.computeSize(SWT.DEFAULT,
+				SWT.DEFAULT, true).x);
+		b.setLayoutData(data);
+		final Text formatToInsert = format;
+		b.addListener(SWT.Selection, new Listener() {
+			public void handleEvent(Event event) {
+				addVariables(formatToInsert, supportedBindings);
+			}
+		});
+
+		return new TextPair(format, null);
+	}
+
+	/**
+	 * Initializes states of the controls from the preference store.
+	 */
+	private void initializeValues() {
+		final IPreferenceStore store = getPreferenceStore();
+
+		fileTextFormat.setText(store
+				.getString(UIPreferences.DECORATOR_FILETEXT_DECORATION));
+		folderTextFormat.setText(store
+				.getString(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION));
+		projectTextFormat.setText(store
+				.getString(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION));
+
+		showDirty.setSelection(store
+				.getBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY));
+
+		setValid(true);
+	}
+
+	/**
+	 * @see IWorkbenchPreferencePage#init(IWorkbench)
+	 */
+	public void init(IWorkbench workbench) {
+		// No-op
+	}
+
+	/**
+	 * OK was clicked. Store the preferences to the plugin store
+	 * 
+	 * @return whether it is okay to close the preference page
+	 */
+	public boolean performOk() {
+		IPreferenceStore store = getPreferenceStore();
+		final boolean okToClose = performOk(store);
+		if (store.needsSaving()) {
+			Activator.getDefault().savePluginPreferences();
+			Activator.broadcastPropertyChange(new PropertyChangeEvent(this,
+					Activator.DECORATORS_CHANGED, null, null));
+		}
+		return okToClose;
+	}
+
+	/**
+	 * Store the preferences to the given preference store
+	 * 
+	 * @param store
+	 *            the preference store to store the preferences to
+	 * 
+	 * @return whether it operation succeeded
+	 */
+	private boolean performOk(IPreferenceStore store) {
+
+		store.setValue(UIPreferences.DECORATOR_FILETEXT_DECORATION,
+				fileTextFormat.getText());
+		store.setValue(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION,
+				folderTextFormat.getText());
+		store.setValue(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION,
+				projectTextFormat.getText());
+
+		store.setValue(UIPreferences.DECORATOR_CALCULATE_DIRTY, showDirty
+				.getSelection());
+
+		return true;
+	}
+
+	/**
+	 * Defaults was clicked. Restore the Git decoration preferences to their
+	 * default values
+	 */
+	protected void performDefaults() {
+		super.performDefaults();
+		IPreferenceStore store = getPreferenceStore();
+
+		fileTextFormat.setText(store
+				.getDefaultString(UIPreferences.DECORATOR_FILETEXT_DECORATION));
+		folderTextFormat
+				.setText(store
+						.getDefaultString(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION));
+		projectTextFormat
+				.setText(store
+						.getDefaultString(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION));
+
+		showDirty.setSelection(store
+				.getDefaultBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY));
+	}
+
+	/**
+	 * Returns the preference store that belongs to the our plugin.
+	 * 
+	 * This is important because we want to store our preferences separately
+	 * from the desktop.
+	 * 
+	 * @return the preference store for this plugin
+	 */
+	protected IPreferenceStore doGetPreferenceStore() {
+		return Activator.getDefault().getPreferenceStore();
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see org.eclipse.jface.dialogs.DialogPage#dispose()
+	 */
+	public void dispose() {
+		PlatformUI.getWorkbench().getThemeManager()
+				.removePropertyChangeListener(fThemeListener);
+		super.dispose();
+	}
+
+	private static class ThemeListener implements IPropertyChangeListener {
+		private final Preview preview;
+
+		ThemeListener(Preview preview) {
+			this.preview = preview;
+		}
+
+		public void propertyChange(PropertyChangeEvent event) {
+			preview.refresh();
+		}
+	}
+
+	/**
+	 * Adds another variable to the given target text
+	 * 
+	 * A ListSelectionDialog pops up and allow the user to choose the variable,
+	 * which is then inserted at current position in <code>text</code>
+	 * 
+	 * @param target
+	 *            the target to add the variable to
+	 * @param bindings
+	 *            the map of bindings
+	 */
+	private void addVariables(Text target, Map bindings) {
+
+		final List<StringPair> variables = new ArrayList<StringPair>(bindings
+				.size());
+
+		ILabelProvider labelProvider = new LabelProvider() {
+			public String getText(Object element) {
+				return ((StringPair) element).s1
+						+ " - " + ((StringPair) element).s2; //$NON-NLS-1$
+			}
+		};
+
+		IStructuredContentProvider contentsProvider = new IStructuredContentProvider() {
+			public Object[] getElements(Object inputElement) {
+				return variables.toArray(new StringPair[variables.size()]);
+			}
+
+			public void dispose() {
+				// No-op
+			}
+
+			public void inputChanged(Viewer viewer, Object oldInput,
+					Object newInput) {
+				// No-op
+			}
+		};
+
+		for (Iterator it = bindings.keySet().iterator(); it.hasNext();) {
+			StringPair variable = new StringPair();
+			variable.s1 = (String) it.next(); // variable
+			variable.s2 = (String) bindings.get(variable.s1); // description
+			variables.add(variable);
+		}
+
+		ListSelectionDialog dialog = new ListSelectionDialog(this.getShell(),
+				this, contentsProvider, labelProvider,
+				UIText.DecoratorPreferencesPage_selectVariablesToAdd);
+		dialog.setTitle(UIText.DecoratorPreferencesPage_addVariablesTitle);
+		if (dialog.open() != Window.OK)
+			return;
+
+		Object[] result = dialog.getResult();
+
+		for (int i = 0; i < result.length; i++) {
+			target.insert("{" + ((StringPair) result[i]).s1 + "}"); //$NON-NLS-1$ //$NON-NLS-2$
+		}
+	}
+
+	class StringPair {
+		String s1;
+
+		String s2;
+	}
+
+	class TextPair {
+		TextPair(Text t1, Text t2) {
+			this.t1 = t1;
+			this.t2 = t2;
+		}
+
+		Text t1;
+
+		Text t2;
+	}
+
+	/**
+	 * Gets the map of bindings between variables and description, to use for
+	 * the format editors for files
+	 * 
+	 * @return the bindings
+	 */
+	private Map getFileBindingDescriptions() {
+		Map<String, String> bindings = new HashMap<String, String>();
+		bindings.put(DecorationHelper.BINDING_RESOURCE_NAME,
+				UIText.DecoratorPreferencesPage_nameResourceVariable);
+		return bindings;
+	}
+
+	/**
+	 * Gets the map of bindings between variables and description, to use for
+	 * the format editors for folders
+	 * 
+	 * @return the bindings
+	 */
+	private Map getFolderBindingDescriptions() {
+		Map<String, String> bindings = new HashMap<String, String>();
+		bindings.put(DecorationHelper.BINDING_RESOURCE_NAME,
+				UIText.DecoratorPreferencesPage_nameResourceVariable);
+		return bindings;
+	}
+
+	/**
+	 * Gets the map of bindings between variables and description, to use for
+	 * the format editors for projects
+	 * 
+	 * @return the bindings
+	 */
+	private Map getProjectBindingDescriptions() {
+		Map<String, String> bindings = new HashMap<String, String>();
+		bindings.put(DecorationHelper.BINDING_RESOURCE_NAME,
+				UIText.DecoratorPreferencesPage_nameResourceVariable);
+		return bindings;
+	}
+
+	private void updatePreview() {
+		if (fPreview != null)
+			fPreview.refresh();
+	}
+
+	/**
+	 * Preview control for showing how changes in the dialog will affect
+	 * decoration
+	 */
+	private class Preview extends LabelProvider implements Observer,
+			ITreeContentProvider {
+
+		private final ResourceManager fImageCache;
+
+		private final TreeViewer fViewer;
+
+		private DecorationHelper fHelper;
+
+		public Preview(Composite composite) {
+			reloadDecorationHelper(); // Has to happen before the tree control
+										// is constructed
+			SWTUtils.createLabel(composite,
+					UIText.DecoratorPreferencesPage_preview);
+			fImageCache = new LocalResourceManager(JFaceResources
+					.getResources());
+			fViewer = new TreeViewer(composite);
+			GridData previewGridData = SWTUtils.createHVFillGridData();
+			previewGridData.heightHint = 100;
+			fViewer.getControl().setLayoutData(previewGridData);
+			fViewer.setContentProvider(this);
+			fViewer.setLabelProvider(this);
+			fViewer.setInput(PREVIEW_FILESYSTEM_ROOT);
+			fViewer.expandAll();
+			fHelper = new DecorationHelper(new PreferenceStore());
+		}
+
+		private void reloadDecorationHelper() {
+			PreferenceStore store = new PreferenceStore();
+			performOk(store);
+			fHelper = new DecorationHelper(store);
+		}
+
+		public void refresh() {
+			reloadDecorationHelper();
+			fViewer.refresh(true);
+			setColorsAndFonts(fViewer.getTree().getItems());
+		}
+
+		@SuppressWarnings("unused")
+		private void setColorsAndFonts(TreeItem[] items) {
+			// TODO: Implement colors and fonts
+		}
+
+		public void update(Observable o, Object arg) {
+			refresh();
+		}
+
+		public Object[] getChildren(Object parentElement) {
+			return ((PreviewResource) parentElement).children.toArray();
+		}
+
+		public Object getParent(Object element) {
+			return null;
+		}
+
+		public boolean hasChildren(Object element) {
+			return !((PreviewResource) element).children.isEmpty();
+		}
+
+		public Object[] getElements(Object inputElement) {
+			return ((Collection) inputElement).toArray();
+		}
+
+		public void dispose() {
+			fImageCache.dispose();
+		}
+
+		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+			// No-op
+		}
+
+		public Color getBackground(Object element) {
+			return getDecoration(element).getBackgroundColor();
+		}
+
+		public Color getForeground(Object element) {
+			return getDecoration(element).getForegroundColor();
+		}
+
+		public Font getFont(Object element) {
+			return getDecoration(element).getFont();
+		}
+
+		public String getText(Object element) {
+			final PreviewDecoration decoration = getDecoration(element);
+			final StringBuffer buffer = new StringBuffer();
+			final String prefix = decoration.getPrefix();
+			if (prefix != null)
+				buffer.append(prefix);
+			buffer.append(((PreviewResource) element).getName());
+			final String suffix = decoration.getSuffix();
+			if (suffix != null)
+				buffer.append(suffix);
+			return buffer.toString();
+		}
+
+		public Image getImage(Object element) {
+			final String s;
+			switch (((PreviewResource) element).type) {
+			case IResource.PROJECT:
+				s = SharedImages.IMG_OBJ_PROJECT;
+				break;
+			case IResource.FOLDER:
+				s = ISharedImages.IMG_OBJ_FOLDER;
+				break;
+			default:
+				s = ISharedImages.IMG_OBJ_FILE;
+				break;
+			}
+			final Image baseImage = PlatformUI.getWorkbench().getSharedImages()
+					.getImage(s);
+			final ImageDescriptor overlay = getDecoration(element).getOverlay();
+			if (overlay == null)
+				return baseImage;
+			try {
+				return fImageCache.createImage(new DecorationOverlayIcon(
+						baseImage, overlay, IDecoration.BOTTOM_RIGHT));
+			} catch (Exception e) {
+				Activator.logError(e.getMessage(), e);
+			}
+
+			return null;
+		}
+
+		private PreviewDecoration getDecoration(Object element) {
+			PreviewDecoration decoration = new PreviewDecoration();
+			fHelper.decorate(decoration, (PreviewResource) element);
+			return decoration;
+		}
+	}
+
+	private static class PreviewResource implements IDecoratableResource {
+		public final String name;
+
+		public final int type;
+
+		public Collection children;
+
+		public PreviewResource(String name, int type) {
+			this.name = name;
+			this.type = type;
+			this.children = Collections.EMPTY_LIST;
+		}
+
+		public String getName() {
+			return name;
+		}
+
+		public int getType() {
+			return type;
+		}
+	}
+
+	private class PreviewDecoration implements IDecoration {
+
+		private List<String> prefixes = new ArrayList<String>();
+
+		private List<String> suffixes = new ArrayList<String>();
+
+		private ImageDescriptor overlay = null;
+
+		private Font font;
+
+		private Color backgroundColor;
+
+		private Color foregroundColor;
+
+		public void addOverlay(ImageDescriptor overlayImage) {
+			overlay = overlayImage;
+		}
+
+		public void addOverlay(ImageDescriptor overlayImage, int quadrant) {
+			overlay = overlayImage;
+		}
+
+		public void addPrefix(String prefix) {
+			prefixes.add(prefix);
+		}
+
+		public void addSuffix(String suffix) {
+			suffixes.add(suffix);
+		}
+
+		public IDecorationContext getDecorationContext() {
+			return new DecorationContext();
+		}
+
+		public void setBackgroundColor(Color color) {
+			backgroundColor = color;
+		}
+
+		public void setForegroundColor(Color color) {
+			foregroundColor = color;
+		}
+
+		public void setFont(Font font) {
+			this.font = font;
+		}
+
+		public ImageDescriptor getOverlay() {
+			return overlay;
+		}
+
+		public String getPrefix() {
+			StringBuffer sb = new StringBuffer();
+			for (Iterator<String> iter = prefixes.iterator(); iter.hasNext();) {
+				sb.append(iter.next());
+			}
+			return sb.toString();
+		}
+
+		public String getSuffix() {
+			StringBuffer sb = new StringBuffer();
+			for (Iterator<String> iter = suffixes.iterator(); iter.hasNext();) {
+				sb.append(iter.next());
+			}
+			return sb.toString();
+		}
+
+		public Font getFont() {
+			return font;
+		}
+
+		public Color getBackgroundColor() {
+			return backgroundColor;
+		}
+
+		public Color getForegroundColor() {
+			return foregroundColor;
+		}
+	}
+}
diff --git a/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties b/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties
index a86e58b..4a0a387 100644
--- a/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties
+++ b/org.spearce.egit.ui/src/org/spearce/egit/ui/uitext.properties
@@ -169,7 +169,6 @@ RefSpecPage_annotatedTagsAutoFollow=Automatically follow tags if we fetch the th
 RefSpecPage_annotatedTagsFetchTags=Always fetch tags, even if we do not have the thing it points at
 RefSpecPage_annotatedTagsNoTags=Never fetch tags, even if we have the thing it points at
 
-Decorator_failedLazyLoading=Resource decorator failed to load tree contents on demand.
 QuickDiff_failedLoading=Quick diff failed to obtain file data.
 
 ResourceHistory_toggleCommentWrap=Wrap Comments
@@ -340,3 +339,27 @@ BranchSelectionDialog_ResetTypeMixed=&Mixed (working directory unmodified)
 BranchSelectionDialog_ResetTypeSoft=&Soft (Index and working directory unmodified)
 BranchSelectionDialog_Tags=Tags
 BranchSelectionDialog_Refs=&Refs
+
+Decorator_exceptionMessage=Errors occurred while applying Git decorations to resources.
+
+DecoratorPreferencesPage_addVariablesTitle=Add Variables
+DecoratorPreferencesPage_addVariablesAction=Add &Variables...
+DecoratorPreferencesPage_computeDeep=Include &ancestors when re-decorating changed resources
+DecoratorPreferencesPage_description=Shows Git specific information on resources in projects under version control.
+
+DecoratorPreferencesPage_decorationSettings=Decoration &settings:
+DecoratorPreferencesPage_preview=Preview:
+DecoratorPreferencesPage_fileFormatLabel=&Files:
+DecoratorPreferencesPage_folderFormatLabel=F&olders:
+DecoratorPreferencesPage_projectFormatLabel=&Projects:
+DecoratorPreferencesPage_fileFormatDefault={name}
+DecoratorPreferencesPage_folderFormatDefault={name}
+DecoratorPreferencesPage_projectFormatDefault={name}
+DecoratorPreferencesPage_labelDecorationsLink=See <a>''{0}''</a> to enable or disable Git decorations.
+DecoratorPreferencesPage_generalTabFolder=&General
+DecoratorPreferencesPage_nameResourceVariable=name of the resource being decorated
+DecoratorPreferencesPage_selectFormats=Select the format for file, folders, and project text labels:
+DecoratorPreferencesPage_selectVariablesToAdd=Select the &variables to add to the decoration format:
+DecoratorPreferencesPage_textLabel=T&ext Decorations
+DecoratorPreferencesPage_iconLabel=&Icon Decorations
+
-- 
1.6.1.2.309.g2ea3