Add binding for name of the current branch
[egit/torarne.git] / org.spearce.egit.ui / src / org / spearce / egit / ui / internal / decorators / GitLightweightDecorator.java
blob265d5a3b5005353a78fe863ca86e9bf27897fb4c
1 /*******************************************************************************
2 * Copyright (C) 2007, IBM Corporation and others
3 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
4 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
5 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
6 * Copyright (C) 2008, Google Inc.
7 * Copyright (C) 2008, Tor Arne Vestbø <torarnv@gmail.com>
9 * All rights reserved. This program and the accompanying materials
10 * are made available under the terms of the Eclipse Public License v1.0
11 * See LICENSE for the full license text, also available.
12 *******************************************************************************/
14 package org.spearce.egit.ui.internal.decorators;
16 import java.io.IOException;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
24 import org.eclipse.core.resources.IProject;
25 import org.eclipse.core.resources.IResource;
26 import org.eclipse.core.resources.IResourceChangeEvent;
27 import org.eclipse.core.resources.IResourceChangeListener;
28 import org.eclipse.core.resources.IResourceDelta;
29 import org.eclipse.core.resources.IResourceDeltaVisitor;
30 import org.eclipse.core.resources.IResourceVisitor;
31 import org.eclipse.core.resources.ResourcesPlugin;
32 import org.eclipse.core.resources.mapping.ResourceMapping;
33 import org.eclipse.core.runtime.CoreException;
34 import org.eclipse.core.runtime.IAdaptable;
35 import org.eclipse.core.runtime.IStatus;
36 import org.eclipse.jface.preference.IPreferenceStore;
37 import org.eclipse.jface.util.IPropertyChangeListener;
38 import org.eclipse.jface.util.PropertyChangeEvent;
39 import org.eclipse.jface.viewers.IDecoration;
40 import org.eclipse.jface.viewers.ILightweightLabelDecorator;
41 import org.eclipse.jface.viewers.LabelProvider;
42 import org.eclipse.jface.viewers.LabelProviderChangedEvent;
43 import org.eclipse.osgi.util.TextProcessor;
44 import org.eclipse.swt.widgets.Display;
45 import org.eclipse.team.ui.TeamUI;
46 import org.eclipse.ui.IContributorResourceAdapter;
47 import org.eclipse.ui.PlatformUI;
48 import org.spearce.egit.core.GitException;
49 import org.spearce.egit.core.internal.util.ExceptionCollector;
50 import org.spearce.egit.core.project.GitProjectData;
51 import org.spearce.egit.core.project.RepositoryChangeListener;
52 import org.spearce.egit.core.project.RepositoryMapping;
53 import org.spearce.egit.ui.Activator;
54 import org.spearce.egit.ui.UIPreferences;
55 import org.spearce.egit.ui.UIText;
56 import org.spearce.jgit.lib.IndexChangedEvent;
57 import org.spearce.jgit.lib.RefsChangedEvent;
58 import org.spearce.jgit.lib.Repository;
59 import org.spearce.jgit.lib.RepositoryChangedEvent;
60 import org.spearce.jgit.lib.RepositoryListener;
62 /**
63 * Supplies annotations for displayed resources
65 * This decorator provides annotations to indicate the status of each resource
66 * when compared to <code>HEAD</code>, as well as the index in the relevant
67 * repository.
69 * TODO: Add support for colors and font decoration
71 public class GitLightweightDecorator extends LabelProvider implements
72 ILightweightLabelDecorator, IPropertyChangeListener,
73 IResourceChangeListener, RepositoryChangeListener, RepositoryListener {
75 /**
76 * Property constant pointing back to the extension point id of the
77 * decorator
79 public static final String DECORATOR_ID = "org.spearce.egit.ui.internal.decorators.GitLightweightDecorator"; //$NON-NLS-1$
81 /**
82 * Bit-mask describing interesting changes for IResourceChangeListener
83 * events
85 private static int INTERESTING_CHANGES = IResourceDelta.CONTENT
86 | IResourceDelta.MOVED_FROM | IResourceDelta.MOVED_TO
87 | IResourceDelta.OPEN | IResourceDelta.REPLACED
88 | IResourceDelta.TYPE;
90 /**
91 * Collector for keeping the error view from filling up with exceptions
93 private static ExceptionCollector exceptions = new ExceptionCollector(
94 UIText.Decorator_exceptionMessage, Activator.getPluginId(),
95 IStatus.ERROR, Activator.getDefault().getLog());
97 /**
98 * Constructs a new Git resource decorator
100 public GitLightweightDecorator() {
101 TeamUI.addPropertyChangeListener(this);
102 Activator.addPropertyChangeListener(this);
103 PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
104 .addPropertyChangeListener(this);
105 Repository.addAnyRepositoryChangedListener(this);
106 GitProjectData.addRepositoryChangeListener(this);
107 ResourcesPlugin.getWorkspace().addResourceChangeListener(this,
108 IResourceChangeEvent.POST_CHANGE);
112 * (non-Javadoc)
114 * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
116 @Override
117 public void dispose() {
118 super.dispose();
119 PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
120 .removePropertyChangeListener(this);
121 TeamUI.removePropertyChangeListener(this);
122 Activator.removePropertyChangeListener(this);
123 Repository.removeAnyRepositoryChangedListener(this);
124 GitProjectData.removeRepositoryChangeListener(this);
125 ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
129 * This method should only be called by the decorator thread.
131 * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang.Object,
132 * org.eclipse.jface.viewers.IDecoration)
134 public void decorate(Object element, IDecoration decoration) {
135 final IResource resource = getResource(element);
136 if (resource == null)
137 return;
139 // Don't decorate the workspace root
140 if (resource.getType() == IResource.ROOT)
141 return;
143 // Don't decorate non-existing resources
144 if (!resource.exists() && !resource.isPhantom())
145 return;
147 // Make sure we're dealing with a Git project
148 final RepositoryMapping mapping = RepositoryMapping
149 .getMapping(resource);
150 if (mapping == null)
151 return;
153 // Cannot decorate linked resources
154 if (mapping.getRepoRelativePath(resource) == null)
155 return;
157 // Don't decorate if UI plugin is not running
158 Activator activator = Activator.getDefault();
159 if (activator == null)
160 return;
162 try {
163 DecorationHelper helper = new DecorationHelper(activator
164 .getPreferenceStore());
165 helper.decorate(decoration,
166 new DecoratableResourceAdapter(resource));
167 } catch (IOException e) {
168 handleException(resource, GitException.wrapException(e));
172 private class DecoratableResourceAdapter implements IDecoratableResource {
174 private IResource resource;
175 private String branch;
177 public DecoratableResourceAdapter(IResource resourceToWrap) throws IOException {
178 resource = resourceToWrap;
179 RepositoryMapping mapping = RepositoryMapping.getMapping(resource);
180 Repository repository = mapping.getRepository();
181 branch = repository.getBranch();
184 public String getName() {
185 return resource.getName();
188 public int getType() {
189 return resource.getType();
192 public String getBranch() {
193 return branch;
198 * Helper class for doing resource decoration, based on the given
199 * preferences
201 * Used for real-time decoration, as well as in the decorator preview
202 * preferences page
204 public static class DecorationHelper {
206 private IPreferenceStore store;
208 /** */
209 public static final String BINDING_RESOURCE_NAME = "name"; //$NON-NLS-1$
210 /** */
211 public static final String BINDING_BRANCH_NAME = "branch"; //$NON-NLS-1$
214 * Constructs a decorator using the rules from the given
215 * <code>preferencesStore</code>
217 * @param preferencesStore
218 * the preferences store with the preferred decorator rules
220 public DecorationHelper(IPreferenceStore preferencesStore) {
221 store = preferencesStore;
225 * Decorates the given <code>decoration</code> based on the state of the
226 * given <code>resource</code>, using the preferences passed when
227 * constructing this decoration helper.
229 * @param decoration
230 * the decoration to decorate
231 * @param resource
232 * the resource to retrieve state from
234 public void decorate(IDecoration decoration,
235 IDecoratableResource resource) {
236 String format = "";
237 switch (resource.getType()) {
238 case IResource.FILE:
239 format = store
240 .getString(UIPreferences.DECORATOR_FILETEXT_DECORATION);
241 break;
242 case IResource.FOLDER:
243 format = store
244 .getString(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION);
245 break;
246 case IResource.PROJECT:
247 format = store
248 .getString(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION);
249 break;
252 Map<String, String> bindings = new HashMap<String, String>();
253 bindings.put(BINDING_RESOURCE_NAME, resource.getName());
254 bindings.put(BINDING_BRANCH_NAME, resource.getBranch());
256 decorate(decoration, format, bindings);
260 * Decorates the given <code>decoration</code>, using the given
261 * <code>format</code>, and mapped using <code>bindings</code>
263 * @param decoration
264 * the decoration to decorate
265 * @param format
266 * the format to base the decoration on
267 * @param bindings
268 * the bindings between variables in the format and actual
269 * values
271 public static void decorate(IDecoration decoration, String format,
272 Map bindings) {
273 StringBuffer prefix = new StringBuffer();
274 StringBuffer suffix = new StringBuffer();
275 StringBuffer output = prefix;
277 int length = format.length();
278 int start = -1;
279 int end = length;
280 while (true) {
281 if ((end = format.indexOf('{', start)) > -1) {
282 output.append(format.substring(start + 1, end));
283 if ((start = format.indexOf('}', end)) > -1) {
284 String key = format.substring(end + 1, start);
285 String s;
287 // We use the BINDING_RESOURCE_NAME key to determine if
288 // we are doing the prefix or suffix. The name isn't
289 // actually part of either.
290 if (key.equals(BINDING_RESOURCE_NAME)) {
291 output = suffix;
292 s = null;
293 } else {
294 s = (String) bindings.get(key);
297 if (s != null) {
298 output.append(s);
299 } else {
300 // Support removing prefix character if binding is
301 // null
302 int curLength = output.length();
303 if (curLength > 0) {
304 char c = output.charAt(curLength - 1);
305 if (c == ':' || c == '@') {
306 output.deleteCharAt(curLength - 1);
310 } else {
311 output.append(format.substring(end, length));
312 break;
314 } else {
315 output.append(format.substring(start + 1, length));
316 break;
320 String prefixString = prefix.toString().replaceAll("^\\s+", "");
321 if (prefixString != null) {
322 decoration.addPrefix(TextProcessor.process(prefixString,
323 "()[].")); //$NON-NLS-1$
325 String suffixString = suffix.toString().replaceAll("\\s+$", "");
326 if (suffixString != null) {
327 decoration.addSuffix(TextProcessor.process(suffixString,
328 "()[].")); //$NON-NLS-1$
333 // -------- Refresh handling --------
336 * Perform a blanket refresh of all decorations
338 public static void refresh() {
339 Display.getDefault().asyncExec(new Runnable() {
340 public void run() {
341 Activator.getDefault().getWorkbench().getDecoratorManager()
342 .update(DECORATOR_ID);
348 * Callback for IPropertyChangeListener events
350 * If any of the relevant preferences has been changed we refresh all
351 * decorations (all projects and their resources).
353 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
355 public void propertyChange(PropertyChangeEvent event) {
356 final String prop = event.getProperty();
357 // If the property is of any interest to us
358 if (prop.equals(TeamUI.GLOBAL_IGNORES_CHANGED)
359 || prop.equals(TeamUI.GLOBAL_FILE_TYPES_CHANGED)
360 || prop.equals(Activator.DECORATORS_CHANGED)) {
361 postLabelEvent(new LabelProviderChangedEvent(this, null /* all */));
366 * Callback for IResourceChangeListener events
368 * Schedules a refresh of the changed resource
370 * If the preference for computing deep dirty states has been set we walk
371 * the ancestor tree of the changed resource and update all parents as well.
373 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
375 public void resourceChanged(IResourceChangeEvent event) {
376 final Set<IResource> resourcesToUpdate = new HashSet<IResource>();
378 try { // Compute the changed resources by looking at the delta
379 event.getDelta().accept(new IResourceDeltaVisitor() {
380 public boolean visit(IResourceDelta delta) throws CoreException {
381 final IResource resource = delta.getResource();
383 if (resource.getType() == IResource.ROOT) {
384 // Continue with the delta
385 return true;
388 if (resource.getType() == IResource.PROJECT) {
389 // If the project is not accessible, don't process it
390 if (!resource.isAccessible())
391 return false;
394 // If the file has changed but not in a way that we care
395 // about
396 // (e.g. marker changes to files) then ignore the change
397 if (delta.getKind() == IResourceDelta.CHANGED
398 && (delta.getFlags() & INTERESTING_CHANGES) == 0) {
399 return true;
402 // All seems good, schedule the resource for update
403 resourcesToUpdate.add(resource);
404 return true;
406 }, true /* includePhantoms */);
407 } catch (final CoreException e) {
408 handleException(null, e);
411 // If deep decorator calculation is enabled in the preferences we
412 // walk the ancestor tree of each of the changed resources and add
413 // their parents to the update set
414 final IPreferenceStore store = Activator.getDefault()
415 .getPreferenceStore();
416 if (store.getBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY)) {
417 final IResource[] changedResources = resourcesToUpdate
418 .toArray(new IResource[resourcesToUpdate.size()]);
419 for (int i = 0; i < changedResources.length; i++) {
420 IResource current = changedResources[i];
421 while (current.getType() != IResource.ROOT) {
422 current = current.getParent();
423 resourcesToUpdate.add(current);
428 postLabelEvent(new LabelProviderChangedEvent(this, resourcesToUpdate
429 .toArray()));
433 * Callback for RepositoryListener events
435 * We resolve the repository mapping for the changed repository and forward
436 * that to repositoryChanged(RepositoryMapping).
438 * @param e
439 * The original change event
441 private void repositoryChanged(RepositoryChangedEvent e) {
442 final Set<RepositoryMapping> ms = new HashSet<RepositoryMapping>();
443 for (final IProject p : ResourcesPlugin.getWorkspace().getRoot()
444 .getProjects()) {
445 final RepositoryMapping mapping = RepositoryMapping.getMapping(p);
446 if (mapping != null && mapping.getRepository() == e.getRepository())
447 ms.add(mapping);
449 for (final RepositoryMapping m : ms) {
450 repositoryChanged(m);
455 * (non-Javadoc)
457 * @see
458 * org.spearce.jgit.lib.RepositoryListener#indexChanged(org.spearce.jgit
459 * .lib.IndexChangedEvent)
461 public void indexChanged(IndexChangedEvent e) {
462 repositoryChanged(e);
466 * (non-Javadoc)
468 * @see
469 * org.spearce.jgit.lib.RepositoryListener#refsChanged(org.spearce.jgit.
470 * lib.RefsChangedEvent)
472 public void refsChanged(RefsChangedEvent e) {
473 repositoryChanged(e);
477 * Callback for RepositoryChangeListener events, as well as
478 * RepositoryListener events via repositoryChanged()
480 * We resolve the project and schedule a refresh of each resource in the
481 * project.
483 * @see org.spearce.egit.core.project.RepositoryChangeListener#repositoryChanged(org.spearce.egit.core.project.RepositoryMapping)
485 public void repositoryChanged(RepositoryMapping mapping) {
486 final IProject project = mapping.getContainer().getProject();
487 if (project == null)
488 return;
490 final List<IResource> resources = new ArrayList<IResource>();
491 try {
492 project.accept(new IResourceVisitor() {
493 public boolean visit(IResource resource) {
494 resources.add(resource);
495 return true;
498 postLabelEvent(new LabelProviderChangedEvent(this, resources
499 .toArray()));
500 } catch (final CoreException e) {
501 handleException(project, e);
505 // -------- Helper methods --------
507 private static IResource getResource(Object element) {
508 if (element instanceof ResourceMapping) {
509 element = ((ResourceMapping) element).getModelObject();
512 IResource resource = null;
513 if (element instanceof IResource) {
514 resource = (IResource) element;
515 } else if (element instanceof IAdaptable) {
516 final IAdaptable adaptable = (IAdaptable) element;
517 resource = (IResource) adaptable.getAdapter(IResource.class);
518 if (resource == null) {
519 final IContributorResourceAdapter adapter = (IContributorResourceAdapter) adaptable
520 .getAdapter(IContributorResourceAdapter.class);
521 if (adapter != null)
522 resource = adapter.getAdaptedResource(adaptable);
526 return resource;
530 * Post the label event to the UI thread
532 * @param event
533 * The event to post
535 private void postLabelEvent(final LabelProviderChangedEvent event) {
536 Display.getDefault().asyncExec(new Runnable() {
537 public void run() {
538 fireLabelProviderChanged(event);
544 * Handle exceptions that occur in the decorator. Exceptions are only logged
545 * for resources that are accessible (i.e. exist in an open project).
547 * @param resource
548 * The resource that triggered the exception
549 * @param e
550 * The exception that occurred
552 private static void handleException(IResource resource, CoreException e) {
553 if (resource == null || resource.isAccessible())
554 exceptions.handleException(e);