Add icon decoration for tracked and untracked resources
[egit/torarne.git] / org.spearce.egit.ui / src / org / spearce / egit / ui / internal / decorators / GitLightweightDecorator.java
blobb20070aad90ea8e8f7ee7ba123b4da9061594f66
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.Collections;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
25 import org.eclipse.core.resources.IProject;
26 import org.eclipse.core.resources.IResource;
27 import org.eclipse.core.resources.IResourceChangeEvent;
28 import org.eclipse.core.resources.IResourceChangeListener;
29 import org.eclipse.core.resources.IResourceDelta;
30 import org.eclipse.core.resources.IResourceDeltaVisitor;
31 import org.eclipse.core.resources.IResourceVisitor;
32 import org.eclipse.core.resources.ResourcesPlugin;
33 import org.eclipse.core.resources.mapping.ResourceMapping;
34 import org.eclipse.core.runtime.CoreException;
35 import org.eclipse.core.runtime.IAdaptable;
36 import org.eclipse.core.runtime.IStatus;
37 import org.eclipse.jface.preference.IPreferenceStore;
38 import org.eclipse.jface.resource.ImageDescriptor;
39 import org.eclipse.jface.util.IPropertyChangeListener;
40 import org.eclipse.jface.util.PropertyChangeEvent;
41 import org.eclipse.jface.viewers.IDecoration;
42 import org.eclipse.jface.viewers.ILightweightLabelDecorator;
43 import org.eclipse.jface.viewers.LabelProvider;
44 import org.eclipse.jface.viewers.LabelProviderChangedEvent;
45 import org.eclipse.osgi.util.TextProcessor;
46 import org.eclipse.swt.graphics.ImageData;
47 import org.eclipse.swt.widgets.Display;
48 import org.eclipse.team.core.Team;
49 import org.eclipse.team.ui.ISharedImages;
50 import org.eclipse.team.ui.TeamImages;
51 import org.eclipse.team.ui.TeamUI;
52 import org.eclipse.ui.IContributorResourceAdapter;
53 import org.eclipse.ui.PlatformUI;
54 import org.spearce.egit.core.GitException;
55 import org.spearce.egit.core.internal.util.ExceptionCollector;
56 import org.spearce.egit.core.project.GitProjectData;
57 import org.spearce.egit.core.project.RepositoryChangeListener;
58 import org.spearce.egit.core.project.RepositoryMapping;
59 import org.spearce.egit.ui.Activator;
60 import org.spearce.egit.ui.UIIcons;
61 import org.spearce.egit.ui.UIPreferences;
62 import org.spearce.egit.ui.UIText;
63 import org.spearce.jgit.dircache.DirCache;
64 import org.spearce.jgit.dircache.DirCacheIterator;
65 import org.spearce.jgit.lib.Constants;
66 import org.spearce.jgit.lib.IndexChangedEvent;
67 import org.spearce.jgit.lib.ObjectId;
68 import org.spearce.jgit.lib.RefsChangedEvent;
69 import org.spearce.jgit.lib.Repository;
70 import org.spearce.jgit.lib.RepositoryChangedEvent;
71 import org.spearce.jgit.lib.RepositoryListener;
72 import org.spearce.jgit.revwalk.RevWalk;
73 import org.spearce.jgit.treewalk.EmptyTreeIterator;
74 import org.spearce.jgit.treewalk.TreeWalk;
75 import org.spearce.jgit.treewalk.filter.PathFilterGroup;
77 /**
78 * Supplies annotations for displayed resources
80 * This decorator provides annotations to indicate the status of each resource
81 * when compared to <code>HEAD</code>, as well as the index in the relevant
82 * repository.
84 * TODO: Add support for colors and font decoration
86 public class GitLightweightDecorator extends LabelProvider implements
87 ILightweightLabelDecorator, IPropertyChangeListener,
88 IResourceChangeListener, RepositoryChangeListener, RepositoryListener {
90 /**
91 * Property constant pointing back to the extension point id of the
92 * decorator
94 public static final String DECORATOR_ID = "org.spearce.egit.ui.internal.decorators.GitLightweightDecorator"; //$NON-NLS-1$
96 /**
97 * Bit-mask describing interesting changes for IResourceChangeListener
98 * events
100 private static int INTERESTING_CHANGES = IResourceDelta.CONTENT
101 | IResourceDelta.MOVED_FROM | IResourceDelta.MOVED_TO
102 | IResourceDelta.OPEN | IResourceDelta.REPLACED
103 | IResourceDelta.TYPE;
106 * Collector for keeping the error view from filling up with exceptions
108 private static ExceptionCollector exceptions = new ExceptionCollector(
109 UIText.Decorator_exceptionMessage, Activator.getPluginId(),
110 IStatus.ERROR, Activator.getDefault().getLog());
113 * Constructs a new Git resource decorator
115 public GitLightweightDecorator() {
116 TeamUI.addPropertyChangeListener(this);
117 Activator.addPropertyChangeListener(this);
118 PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
119 .addPropertyChangeListener(this);
120 Repository.addAnyRepositoryChangedListener(this);
121 GitProjectData.addRepositoryChangeListener(this);
122 ResourcesPlugin.getWorkspace().addResourceChangeListener(this,
123 IResourceChangeEvent.POST_CHANGE);
127 * (non-Javadoc)
129 * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
131 @Override
132 public void dispose() {
133 super.dispose();
134 PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
135 .removePropertyChangeListener(this);
136 TeamUI.removePropertyChangeListener(this);
137 Activator.removePropertyChangeListener(this);
138 Repository.removeAnyRepositoryChangedListener(this);
139 GitProjectData.removeRepositoryChangeListener(this);
140 ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
144 * This method should only be called by the decorator thread.
146 * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang.Object,
147 * org.eclipse.jface.viewers.IDecoration)
149 public void decorate(Object element, IDecoration decoration) {
150 final IResource resource = getResource(element);
151 if (resource == null)
152 return;
154 // Don't decorate the workspace root
155 if (resource.getType() == IResource.ROOT)
156 return;
158 // Don't decorate non-existing resources
159 if (!resource.exists() && !resource.isPhantom())
160 return;
162 // Make sure we're dealing with a project under Git revision control
163 final RepositoryMapping mapping = RepositoryMapping
164 .getMapping(resource);
165 if (mapping == null)
166 return;
168 // Cannot decorate linked resources
169 if (mapping.getRepoRelativePath(resource) == null)
170 return;
172 // Don't decorate if UI plugin is not running
173 Activator activator = Activator.getDefault();
174 if (activator == null)
175 return;
177 try {
178 DecorationHelper helper = new DecorationHelper(activator
179 .getPreferenceStore());
180 helper.decorate(decoration,
181 new DecoratableResourceAdapter(resource));
182 } catch (IOException e) {
183 handleException(resource, GitException.wrapException(e));
187 private class DecoratableResourceAdapter implements IDecoratableResource {
189 private final IResource resource;
191 private final RepositoryMapping mapping;
193 private final Repository repository;
195 private final ObjectId headId;
197 private String branch = "";
199 private boolean tracked = false;
201 private boolean ignored = false;
203 public DecoratableResourceAdapter(IResource resourceToWrap)
204 throws IOException {
205 resource = resourceToWrap;
206 mapping = RepositoryMapping.getMapping(resource);
207 repository = mapping.getRepository();
208 headId = repository.resolve(Constants.HEAD);
210 initializeValues();
214 * Initialize the various values that are used for making decoration
215 * decisions later on.
217 * We might as well pre-load these now, instead of using lazy
218 * initialization, because they are all read by the decorator when
219 * building variable bindings and computing the preferred overlay.
221 * @throws IOException
223 private void initializeValues() throws IOException {
225 // Resolve current branch
226 branch = repository.getBranch();
228 // Resolve tracked state
229 if (getType() == IResource.PROJECT) {
230 tracked = true;
231 } else {
232 final TreeWalk treeWalk = new TreeWalk(repository);
234 Set<String> repositoryPaths = Collections.singleton(mapping
235 .getRepoRelativePath(resource));
236 if (!(repositoryPaths.isEmpty() || repositoryPaths.contains(""))) {
237 treeWalk.setFilter(PathFilterGroup
238 .createFromStrings(repositoryPaths));
239 treeWalk.setRecursive(treeWalk.getFilter()
240 .shouldBeRecursive());
241 treeWalk.reset();
243 if (headId != null)
244 treeWalk.addTree(new RevWalk(repository)
245 .parseTree(headId));
246 else
247 treeWalk.addTree(new EmptyTreeIterator());
249 treeWalk.addTree(new DirCacheIterator(DirCache
250 .read(repository)));
251 if (treeWalk.next()) {
252 tracked = true;
257 // Resolve ignored state (currently only reads the global Eclipse
258 // ignores)
259 // TODO: Also read ignores from .git/info/excludes et al.
260 if (Team.isIgnoredHint(resource)) {
261 ignored = true;
265 public String getName() {
266 return resource.getName();
269 public int getType() {
270 return resource.getType();
273 public String getBranch() {
274 return branch;
277 public boolean isTracked() {
278 return tracked;
281 public boolean isIgnored() {
282 return ignored;
287 * Helper class for doing resource decoration, based on the given
288 * preferences
290 * Used for real-time decoration, as well as in the decorator preview
291 * preferences page
293 public static class DecorationHelper {
295 /** */
296 public static final String BINDING_RESOURCE_NAME = "name"; //$NON-NLS-1$
298 /** */
299 public static final String BINDING_BRANCH_NAME = "branch"; //$NON-NLS-1$
301 private IPreferenceStore store;
304 * Define a cached image descriptor which only creates the image data
305 * once
307 private static class CachedImageDescriptor extends ImageDescriptor {
308 ImageDescriptor descriptor;
310 ImageData data;
312 public CachedImageDescriptor(ImageDescriptor descriptor) {
313 this.descriptor = descriptor;
316 public ImageData getImageData() {
317 if (data == null) {
318 data = descriptor.getImageData();
320 return data;
324 private static ImageDescriptor trackedImage;
326 private static ImageDescriptor untrackedImage;
328 static {
329 trackedImage = new CachedImageDescriptor(TeamImages
330 .getImageDescriptor(ISharedImages.IMG_CHECKEDIN_OVR));
331 untrackedImage = new CachedImageDescriptor(UIIcons.OVR_UNTRACKED);
335 * Constructs a decorator using the rules from the given
336 * <code>preferencesStore</code>
338 * @param preferencesStore
339 * the preferences store with the preferred decorator rules
341 public DecorationHelper(IPreferenceStore preferencesStore) {
342 store = preferencesStore;
346 * Decorates the given <code>decoration</code> based on the state of the
347 * given <code>resource</code>, using the preferences passed when
348 * constructing this decoration helper.
350 * @param decoration
351 * the decoration to decorate
352 * @param resource
353 * the resource to retrieve state from
355 public void decorate(IDecoration decoration,
356 IDecoratableResource resource) {
357 decorateText(decoration, resource);
358 decorateIcons(decoration, resource);
361 private void decorateText(IDecoration decoration,
362 IDecoratableResource resource) {
363 String format = "";
364 switch (resource.getType()) {
365 case IResource.FILE:
366 format = store
367 .getString(UIPreferences.DECORATOR_FILETEXT_DECORATION);
368 break;
369 case IResource.FOLDER:
370 format = store
371 .getString(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION);
372 break;
373 case IResource.PROJECT:
374 format = store
375 .getString(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION);
376 break;
379 Map<String, String> bindings = new HashMap<String, String>();
380 bindings.put(BINDING_RESOURCE_NAME, resource.getName());
381 bindings.put(BINDING_BRANCH_NAME, resource.getBranch());
383 decorate(decoration, format, bindings);
386 private void decorateIcons(IDecoration decoration,
387 IDecoratableResource resource) {
388 if (resource.isIgnored())
389 return;
391 if (resource.isTracked()) {
392 if (store.getBoolean(UIPreferences.DECORATOR_SHOW_TRACKED_ICON))
393 decoration.addOverlay(trackedImage);
394 } else if (store
395 .getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON)) {
396 decoration.addOverlay(untrackedImage);
401 * Decorates the given <code>decoration</code>, using the specified text
402 * <code>format</code>, and mapped using the variable bindings from
403 * <code>bindings</code>
405 * @param decoration
406 * the decoration to decorate
407 * @param format
408 * the format to base the decoration on
409 * @param bindings
410 * the bindings between variables in the format and actual
411 * values
413 public static void decorate(IDecoration decoration, String format,
414 Map bindings) {
415 StringBuffer prefix = new StringBuffer();
416 StringBuffer suffix = new StringBuffer();
417 StringBuffer output = prefix;
419 int length = format.length();
420 int start = -1;
421 int end = length;
422 while (true) {
423 if ((end = format.indexOf('{', start)) > -1) {
424 output.append(format.substring(start + 1, end));
425 if ((start = format.indexOf('}', end)) > -1) {
426 String key = format.substring(end + 1, start);
427 String s;
429 // We use the BINDING_RESOURCE_NAME key to determine if
430 // we are doing the prefix or suffix. The name isn't
431 // actually part of either.
432 if (key.equals(BINDING_RESOURCE_NAME)) {
433 output = suffix;
434 s = null;
435 } else {
436 s = (String) bindings.get(key);
439 if (s != null) {
440 output.append(s);
441 } else {
442 // Support removing prefix character if binding is
443 // null
444 int curLength = output.length();
445 if (curLength > 0) {
446 char c = output.charAt(curLength - 1);
447 if (c == ':' || c == '@') {
448 output.deleteCharAt(curLength - 1);
452 } else {
453 output.append(format.substring(end, length));
454 break;
456 } else {
457 output.append(format.substring(start + 1, length));
458 break;
462 String prefixString = prefix.toString().replaceAll("^\\s+", "");
463 if (prefixString != null) {
464 decoration.addPrefix(TextProcessor.process(prefixString,
465 "()[].")); //$NON-NLS-1$
467 String suffixString = suffix.toString().replaceAll("\\s+$", "");
468 if (suffixString != null) {
469 decoration.addSuffix(TextProcessor.process(suffixString,
470 "()[].")); //$NON-NLS-1$
475 // -------- Refresh handling --------
478 * Perform a blanket refresh of all decorations
480 public static void refresh() {
481 Display.getDefault().asyncExec(new Runnable() {
482 public void run() {
483 Activator.getDefault().getWorkbench().getDecoratorManager()
484 .update(DECORATOR_ID);
490 * Callback for IPropertyChangeListener events
492 * If any of the relevant preferences has been changed we refresh all
493 * decorations (all projects and their resources).
495 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
497 public void propertyChange(PropertyChangeEvent event) {
498 final String prop = event.getProperty();
499 // If the property is of any interest to us
500 if (prop.equals(TeamUI.GLOBAL_IGNORES_CHANGED)
501 || prop.equals(TeamUI.GLOBAL_FILE_TYPES_CHANGED)
502 || prop.equals(Activator.DECORATORS_CHANGED)) {
503 postLabelEvent(new LabelProviderChangedEvent(this, null /* all */));
508 * Callback for IResourceChangeListener events
510 * Schedules a refresh of the changed resource
512 * If the preference for computing deep dirty states has been set we walk
513 * the ancestor tree of the changed resource and update all parents as well.
515 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
517 public void resourceChanged(IResourceChangeEvent event) {
518 final Set<IResource> resourcesToUpdate = new HashSet<IResource>();
520 try { // Compute the changed resources by looking at the delta
521 event.getDelta().accept(new IResourceDeltaVisitor() {
522 public boolean visit(IResourceDelta delta) throws CoreException {
523 final IResource resource = delta.getResource();
525 if (resource.getType() == IResource.ROOT) {
526 // Continue with the delta
527 return true;
530 if (resource.getType() == IResource.PROJECT) {
531 // If the project is not accessible, don't process it
532 if (!resource.isAccessible())
533 return false;
536 // If the file has changed but not in a way that we care
537 // about
538 // (e.g. marker changes to files) then ignore the change
539 if (delta.getKind() == IResourceDelta.CHANGED
540 && (delta.getFlags() & INTERESTING_CHANGES) == 0) {
541 return true;
544 // All seems good, schedule the resource for update
545 resourcesToUpdate.add(resource);
546 return true;
548 }, true /* includePhantoms */);
549 } catch (final CoreException e) {
550 handleException(null, e);
553 // If deep decorator calculation is enabled in the preferences we
554 // walk the ancestor tree of each of the changed resources and add
555 // their parents to the update set
556 final IPreferenceStore store = Activator.getDefault()
557 .getPreferenceStore();
558 if (store.getBoolean(UIPreferences.DECORATOR_CALCULATE_DIRTY)) {
559 final IResource[] changedResources = resourcesToUpdate
560 .toArray(new IResource[resourcesToUpdate.size()]);
561 for (int i = 0; i < changedResources.length; i++) {
562 IResource current = changedResources[i];
563 while (current.getType() != IResource.ROOT) {
564 current = current.getParent();
565 resourcesToUpdate.add(current);
570 postLabelEvent(new LabelProviderChangedEvent(this, resourcesToUpdate
571 .toArray()));
575 * Callback for RepositoryListener events
577 * We resolve the repository mapping for the changed repository and forward
578 * that to repositoryChanged(RepositoryMapping).
580 * @param e
581 * The original change event
583 private void repositoryChanged(RepositoryChangedEvent e) {
584 final Set<RepositoryMapping> ms = new HashSet<RepositoryMapping>();
585 for (final IProject p : ResourcesPlugin.getWorkspace().getRoot()
586 .getProjects()) {
587 final RepositoryMapping mapping = RepositoryMapping.getMapping(p);
588 if (mapping != null && mapping.getRepository() == e.getRepository())
589 ms.add(mapping);
591 for (final RepositoryMapping m : ms) {
592 repositoryChanged(m);
597 * (non-Javadoc)
599 * @see
600 * org.spearce.jgit.lib.RepositoryListener#indexChanged(org.spearce.jgit
601 * .lib.IndexChangedEvent)
603 public void indexChanged(IndexChangedEvent e) {
604 repositoryChanged(e);
608 * (non-Javadoc)
610 * @see
611 * org.spearce.jgit.lib.RepositoryListener#refsChanged(org.spearce.jgit.
612 * lib.RefsChangedEvent)
614 public void refsChanged(RefsChangedEvent e) {
615 repositoryChanged(e);
619 * Callback for RepositoryChangeListener events, as well as
620 * RepositoryListener events via repositoryChanged()
622 * We resolve the project and schedule a refresh of each resource in the
623 * project.
625 * @see org.spearce.egit.core.project.RepositoryChangeListener#repositoryChanged(org.spearce.egit.core.project.RepositoryMapping)
627 public void repositoryChanged(RepositoryMapping mapping) {
628 final IProject project = mapping.getContainer().getProject();
629 if (project == null)
630 return;
632 final List<IResource> resources = new ArrayList<IResource>();
633 try {
634 project.accept(new IResourceVisitor() {
635 public boolean visit(IResource resource) {
636 resources.add(resource);
637 return true;
640 postLabelEvent(new LabelProviderChangedEvent(this, resources
641 .toArray()));
642 } catch (final CoreException e) {
643 handleException(project, e);
647 // -------- Helper methods --------
649 private static IResource getResource(Object element) {
650 if (element instanceof ResourceMapping) {
651 element = ((ResourceMapping) element).getModelObject();
654 IResource resource = null;
655 if (element instanceof IResource) {
656 resource = (IResource) element;
657 } else if (element instanceof IAdaptable) {
658 final IAdaptable adaptable = (IAdaptable) element;
659 resource = (IResource) adaptable.getAdapter(IResource.class);
660 if (resource == null) {
661 final IContributorResourceAdapter adapter = (IContributorResourceAdapter) adaptable
662 .getAdapter(IContributorResourceAdapter.class);
663 if (adapter != null)
664 resource = adapter.getAdaptedResource(adaptable);
668 return resource;
672 * Post the label event to the UI thread
674 * @param event
675 * The event to post
677 private void postLabelEvent(final LabelProviderChangedEvent event) {
678 Display.getDefault().asyncExec(new Runnable() {
679 public void run() {
680 fireLabelProviderChanged(event);
686 * Handle exceptions that occur in the decorator. Exceptions are only logged
687 * for resources that are accessible (i.e. exist in an open project).
689 * @param resource
690 * The resource that triggered the exception
691 * @param e
692 * The exception that occurred
694 private static void handleException(IResource resource, CoreException e) {
695 if (resource == null || resource.isAccessible())
696 exceptions.handleException(e);