Skip to main content
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/decorators/GitLightweightDecorator.java')
-rw-r--r--org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/decorators/GitLightweightDecorator.java655
1 files changed, 655 insertions, 0 deletions
diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/decorators/GitLightweightDecorator.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/decorators/GitLightweightDecorator.java
new file mode 100644
index 0000000000..e68969251a
--- /dev/null
+++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/decorators/GitLightweightDecorator.java
@@ -0,0 +1,655 @@
+/*******************************************************************************
+ * Copyright (C) 2007, IBM Corporation and others
+ * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
+ * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
+ * Copyright (C) 2008, Google Inc.
+ * Copyright (C) 2008, Tor Arne Vestbø <torarnv@gmail.com>
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.egit.ui.internal.decorators;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+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.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.core.runtime.Status;
+import org.eclipse.egit.core.internal.util.ExceptionCollector;
+import org.eclipse.egit.core.project.GitProjectData;
+import org.eclipse.egit.core.project.RepositoryChangeListener;
+import org.eclipse.egit.core.project.RepositoryMapping;
+import org.eclipse.egit.ui.Activator;
+import org.eclipse.egit.ui.UIIcons;
+import org.eclipse.egit.ui.UIPreferences;
+import org.eclipse.egit.ui.UIText;
+import org.eclipse.egit.ui.internal.decorators.IDecoratableResource.Staged;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.resource.ImageDescriptor;
+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.graphics.ImageData;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.team.ui.ISharedImages;
+import org.eclipse.team.ui.TeamImages;
+import org.eclipse.team.ui.TeamUI;
+import org.eclipse.ui.IContributorResourceAdapter;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.jgit.lib.IndexChangedEvent;
+import org.eclipse.jgit.lib.RefsChangedEvent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryChangedEvent;
+import org.eclipse.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.eclipse.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 if the workbench is not running
+ if (!PlatformUI.isWorkbenchRunning())
+ return;
+
+ // Don't decorate if UI plugin is not running
+ Activator activator = Activator.getDefault();
+ if (activator == 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 project under Git revision control
+ final RepositoryMapping mapping = RepositoryMapping
+ .getMapping(resource);
+ if (mapping == null)
+ return;
+
+ // Cannot decorate linked resources
+ if (mapping.getRepoRelativePath(resource) == null)
+ return;
+
+ try {
+ DecorationHelper helper = new DecorationHelper(activator
+ .getPreferenceStore());
+ helper.decorate(decoration,
+ new DecoratableResourceAdapter(resource));
+ } catch (IOException e) {
+ handleException(resource, new CoreException(new Status(
+ IStatus.ERROR, Activator.getPluginId(), e.getMessage(), e)));
+ }
+ }
+
+ /**
+ * 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 {
+
+ /** */
+ public static final String BINDING_RESOURCE_NAME = "name"; //$NON-NLS-1$
+
+ /** */
+ public static final String BINDING_BRANCH_NAME = "branch"; //$NON-NLS-1$
+
+ /** */
+ public static final String BINDING_DIRTY_FLAG = "dirty"; //$NON-NLS-1$
+
+ /** */
+ public static final String BINDING_STAGED_FLAG = "staged"; //$NON-NLS-1$
+
+ private IPreferenceStore store;
+
+ /**
+ * Define a cached image descriptor which only creates the image data
+ * once
+ */
+ private static class CachedImageDescriptor extends ImageDescriptor {
+ ImageDescriptor descriptor;
+
+ ImageData data;
+
+ public CachedImageDescriptor(ImageDescriptor descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ public ImageData getImageData() {
+ if (data == null) {
+ data = descriptor.getImageData();
+ }
+ return data;
+ }
+ }
+
+ private static ImageDescriptor trackedImage;
+
+ private static ImageDescriptor untrackedImage;
+
+ private static ImageDescriptor stagedImage;
+
+ private static ImageDescriptor stagedAddedImage;
+
+ private static ImageDescriptor stagedRemovedImage;
+
+ private static ImageDescriptor conflictImage;
+
+ private static ImageDescriptor assumeValidImage;
+
+ static {
+ trackedImage = new CachedImageDescriptor(TeamImages
+ .getImageDescriptor(ISharedImages.IMG_CHECKEDIN_OVR));
+ untrackedImage = new CachedImageDescriptor(UIIcons.OVR_UNTRACKED);
+ stagedImage = new CachedImageDescriptor(UIIcons.OVR_STAGED);
+ stagedAddedImage = new CachedImageDescriptor(UIIcons.OVR_STAGED_ADD);
+ stagedRemovedImage = new CachedImageDescriptor(
+ UIIcons.OVR_STAGED_REMOVE);
+ conflictImage = new CachedImageDescriptor(UIIcons.OVR_CONFLICT);
+ assumeValidImage = new CachedImageDescriptor(UIIcons.OVR_ASSUMEVALID);
+ }
+
+ /**
+ * 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) {
+ if (resource.isIgnored())
+ return;
+
+ decorateText(decoration, resource);
+ decorateIcons(decoration, resource);
+ }
+
+ private void decorateText(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());
+ bindings.put(BINDING_BRANCH_NAME, resource.getBranch());
+ bindings.put(BINDING_DIRTY_FLAG, resource.isDirty() ? ">" : null);
+ bindings.put(BINDING_STAGED_FLAG,
+ resource.staged() != Staged.NOT_STAGED ? "*" : null);
+
+ decorate(decoration, format, bindings);
+ }
+
+ private void decorateIcons(IDecoration decoration,
+ IDecoratableResource resource) {
+ ImageDescriptor overlay = null;
+
+ if (resource.isTracked()) {
+ if (store.getBoolean(UIPreferences.DECORATOR_SHOW_TRACKED_ICON))
+ overlay = trackedImage;
+
+ if (store
+ .getBoolean(UIPreferences.DECORATOR_SHOW_ASSUME_VALID_ICON)
+ && resource.isAssumeValid())
+ overlay = assumeValidImage;
+
+ // Staged overrides tracked
+ Staged staged = resource.staged();
+ if (store.getBoolean(UIPreferences.DECORATOR_SHOW_STAGED_ICON)
+ && staged != Staged.NOT_STAGED) {
+ if (staged == Staged.ADDED)
+ overlay = stagedAddedImage;
+ else if (staged == Staged.REMOVED)
+ overlay = stagedRemovedImage;
+ else
+ overlay = stagedImage;
+ }
+
+ // Conflicts override everything
+ if (store
+ .getBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON)
+ && resource.hasConflicts())
+ overlay = conflictImage;
+
+ } else if (store
+ .getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON)) {
+ overlay = untrackedImage;
+ }
+
+ // Overlays can only be added once, so do it at the end
+ decoration.addOverlay(overlay);
+ }
+
+ /**
+ * Decorates the given <code>decoration</code>, using the specified text
+ * <code>format</code>, and mapped using the variable bindings from
+ * <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<String, String> 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;
+
+ // Allow users to override the binding
+ if (key.indexOf(':') > -1) {
+ String[] keyAndBinding = key.split(":", 2);
+ key = keyAndBinding[0];
+ if (keyAndBinding.length > 1
+ && bindings.get(key) != null)
+ bindings.put(key, keyAndBinding[1]);
+ }
+
+ // 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 = 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));
+ }
+ }
+
+ /**
+ * 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 {
+
+ // If the file has changed but not in a way that we care
+ // about (e.g. marker changes to files) then ignore
+ if (delta.getKind() == IResourceDelta.CHANGED
+ && (delta.getFlags() & INTERESTING_CHANGES) == 0) {
+ return true;
+ }
+
+ final IResource resource = delta.getResource();
+
+ // If the resource is not part of a project under Git
+ // revision control
+ final RepositoryMapping mapping = RepositoryMapping
+ .getMapping(resource);
+ if (mapping == null) {
+ // Ignore the change
+ return true;
+ }
+
+ 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;
+ }
+
+ // All seems good, schedule the resource for update
+ resourcesToUpdate.add(resource);
+
+ if (delta.getKind() == IResourceDelta.CHANGED
+ && (delta.getFlags() & IResourceDelta.OPEN) > 1)
+ return false; // Don't recurse when opening projects
+ else
+ return true;
+ }
+ }, true /* includePhantoms */);
+ } catch (final CoreException e) {
+ handleException(null, e);
+ }
+
+ if (resourcesToUpdate.isEmpty())
+ return;
+
+ // If ancestor-decoration 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_RECOMPUTE_ANCESTORS)) {
+ final IResource[] changedResources = resourcesToUpdate
+ .toArray(new IResource[resourcesToUpdate.size()]);
+ for (IResource current : changedResources) {
+ 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.eclipse.jgit.lib.RepositoryListener#indexChanged(org.eclipse.jgit
+ * .lib.IndexChangedEvent)
+ */
+ public void indexChanged(IndexChangedEvent e) {
+ repositoryChanged(e);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.eclipse.jgit.lib.RepositoryListener#refsChanged(org.eclipse.jgit.
+ * lib.RefsChangedEvent)
+ */
+ public void refsChanged(RefsChangedEvent e) {
+ repositoryChanged(e);
+ }
+
+ /**
+ * Callback for RepositoryChangeListener events, as well as
+ * RepositoryListener events via repositoryChanged()
+ *
+ * @see org.eclipse.egit.core.project.RepositoryChangeListener#repositoryChanged(org.eclipse.egit.core.project.RepositoryMapping)
+ */
+ public void repositoryChanged(RepositoryMapping mapping) {
+ // Until we find a way to refresh visible labels within a project
+ // we have to use this blanket refresh that includes all projects.
+ postLabelEvent(new LabelProviderChangedEvent(this));
+ }
+
+ // -------- 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);
+ }
+}

Back to the top