Get rid of the class GitException
[egit/chris.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / decorators / GitLightweightDecorator.java
blobbea25fb9af60d3a8809ebf52296185912a666bb5
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 * which accompanies this distribution, and is available at
12 * http://www.eclipse.org/legal/epl-v10.html
13 *******************************************************************************/
15 package org.eclipse.egit.ui.internal.decorators;
17 import java.io.IOException;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.Map;
21 import java.util.Set;
23 import org.eclipse.core.resources.IProject;
24 import org.eclipse.core.resources.IResource;
25 import org.eclipse.core.resources.IResourceChangeEvent;
26 import org.eclipse.core.resources.IResourceChangeListener;
27 import org.eclipse.core.resources.IResourceDelta;
28 import org.eclipse.core.resources.IResourceDeltaVisitor;
29 import org.eclipse.core.resources.ResourcesPlugin;
30 import org.eclipse.core.resources.mapping.ResourceMapping;
31 import org.eclipse.core.runtime.CoreException;
32 import org.eclipse.core.runtime.IAdaptable;
33 import org.eclipse.core.runtime.IStatus;
34 import org.eclipse.core.runtime.Status;
35 import org.eclipse.egit.core.internal.util.ExceptionCollector;
36 import org.eclipse.egit.core.project.GitProjectData;
37 import org.eclipse.egit.core.project.RepositoryChangeListener;
38 import org.eclipse.egit.core.project.RepositoryMapping;
39 import org.eclipse.egit.ui.Activator;
40 import org.eclipse.egit.ui.UIIcons;
41 import org.eclipse.egit.ui.UIPreferences;
42 import org.eclipse.egit.ui.UIText;
43 import org.eclipse.egit.ui.internal.decorators.IDecoratableResource.Staged;
44 import org.eclipse.jface.preference.IPreferenceStore;
45 import org.eclipse.jface.resource.ImageDescriptor;
46 import org.eclipse.jface.util.IPropertyChangeListener;
47 import org.eclipse.jface.util.PropertyChangeEvent;
48 import org.eclipse.jface.viewers.IDecoration;
49 import org.eclipse.jface.viewers.ILightweightLabelDecorator;
50 import org.eclipse.jface.viewers.LabelProvider;
51 import org.eclipse.jface.viewers.LabelProviderChangedEvent;
52 import org.eclipse.osgi.util.TextProcessor;
53 import org.eclipse.swt.graphics.ImageData;
54 import org.eclipse.swt.widgets.Display;
55 import org.eclipse.team.ui.ISharedImages;
56 import org.eclipse.team.ui.TeamImages;
57 import org.eclipse.team.ui.TeamUI;
58 import org.eclipse.ui.IContributorResourceAdapter;
59 import org.eclipse.ui.PlatformUI;
60 import org.spearce.jgit.lib.IndexChangedEvent;
61 import org.spearce.jgit.lib.RefsChangedEvent;
62 import org.spearce.jgit.lib.Repository;
63 import org.spearce.jgit.lib.RepositoryChangedEvent;
64 import org.spearce.jgit.lib.RepositoryListener;
66 /**
67 * Supplies annotations for displayed resources
69 * This decorator provides annotations to indicate the status of each resource
70 * when compared to <code>HEAD</code>, as well as the index in the relevant
71 * repository.
73 * TODO: Add support for colors and font decoration
75 public class GitLightweightDecorator extends LabelProvider implements
76 ILightweightLabelDecorator, IPropertyChangeListener,
77 IResourceChangeListener, RepositoryChangeListener, RepositoryListener {
79 /**
80 * Property constant pointing back to the extension point id of the
81 * decorator
83 public static final String DECORATOR_ID = "org.eclipse.egit.ui.internal.decorators.GitLightweightDecorator"; //$NON-NLS-1$
85 /**
86 * Bit-mask describing interesting changes for IResourceChangeListener
87 * events
89 private static int INTERESTING_CHANGES = IResourceDelta.CONTENT
90 | IResourceDelta.MOVED_FROM | IResourceDelta.MOVED_TO
91 | IResourceDelta.OPEN | IResourceDelta.REPLACED
92 | IResourceDelta.TYPE;
94 /**
95 * Collector for keeping the error view from filling up with exceptions
97 private static ExceptionCollector exceptions = new ExceptionCollector(
98 UIText.Decorator_exceptionMessage, Activator.getPluginId(),
99 IStatus.ERROR, Activator.getDefault().getLog());
102 * Constructs a new Git resource decorator
104 public GitLightweightDecorator() {
105 TeamUI.addPropertyChangeListener(this);
106 Activator.addPropertyChangeListener(this);
107 PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
108 .addPropertyChangeListener(this);
109 Repository.addAnyRepositoryChangedListener(this);
110 GitProjectData.addRepositoryChangeListener(this);
111 ResourcesPlugin.getWorkspace().addResourceChangeListener(this,
112 IResourceChangeEvent.POST_CHANGE);
116 * (non-Javadoc)
118 * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
120 @Override
121 public void dispose() {
122 super.dispose();
123 PlatformUI.getWorkbench().getThemeManager().getCurrentTheme()
124 .removePropertyChangeListener(this);
125 TeamUI.removePropertyChangeListener(this);
126 Activator.removePropertyChangeListener(this);
127 Repository.removeAnyRepositoryChangedListener(this);
128 GitProjectData.removeRepositoryChangeListener(this);
129 ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
133 * This method should only be called by the decorator thread.
135 * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang.Object,
136 * org.eclipse.jface.viewers.IDecoration)
138 public void decorate(Object element, IDecoration decoration) {
139 final IResource resource = getResource(element);
140 if (resource == null)
141 return;
143 // Don't decorate if the workbench is not running
144 if (!PlatformUI.isWorkbenchRunning())
145 return;
147 // Don't decorate if UI plugin is not running
148 Activator activator = Activator.getDefault();
149 if (activator == null)
150 return;
152 // Don't decorate the workspace root
153 if (resource.getType() == IResource.ROOT)
154 return;
156 // Don't decorate non-existing resources
157 if (!resource.exists() && !resource.isPhantom())
158 return;
160 // Make sure we're dealing with a project under Git revision control
161 final RepositoryMapping mapping = RepositoryMapping
162 .getMapping(resource);
163 if (mapping == null)
164 return;
166 // Cannot decorate linked resources
167 if (mapping.getRepoRelativePath(resource) == null)
168 return;
170 try {
171 DecorationHelper helper = new DecorationHelper(activator
172 .getPreferenceStore());
173 helper.decorate(decoration,
174 new DecoratableResourceAdapter(resource));
175 } catch (IOException e) {
176 handleException(resource, new CoreException(new Status(
177 IStatus.ERROR, Activator.getPluginId(), e.getMessage(), e)));
182 * Helper class for doing resource decoration, based on the given
183 * preferences
185 * Used for real-time decoration, as well as in the decorator preview
186 * preferences page
188 public static class DecorationHelper {
190 /** */
191 public static final String BINDING_RESOURCE_NAME = "name"; //$NON-NLS-1$
193 /** */
194 public static final String BINDING_BRANCH_NAME = "branch"; //$NON-NLS-1$
196 /** */
197 public static final String BINDING_DIRTY_FLAG = "dirty"; //$NON-NLS-1$
199 /** */
200 public static final String BINDING_STAGED_FLAG = "staged"; //$NON-NLS-1$
202 private IPreferenceStore store;
205 * Define a cached image descriptor which only creates the image data
206 * once
208 private static class CachedImageDescriptor extends ImageDescriptor {
209 ImageDescriptor descriptor;
211 ImageData data;
213 public CachedImageDescriptor(ImageDescriptor descriptor) {
214 this.descriptor = descriptor;
217 public ImageData getImageData() {
218 if (data == null) {
219 data = descriptor.getImageData();
221 return data;
225 private static ImageDescriptor trackedImage;
227 private static ImageDescriptor untrackedImage;
229 private static ImageDescriptor stagedImage;
231 private static ImageDescriptor stagedAddedImage;
233 private static ImageDescriptor stagedRemovedImage;
235 private static ImageDescriptor conflictImage;
237 private static ImageDescriptor assumeValidImage;
239 static {
240 trackedImage = new CachedImageDescriptor(TeamImages
241 .getImageDescriptor(ISharedImages.IMG_CHECKEDIN_OVR));
242 untrackedImage = new CachedImageDescriptor(UIIcons.OVR_UNTRACKED);
243 stagedImage = new CachedImageDescriptor(UIIcons.OVR_STAGED);
244 stagedAddedImage = new CachedImageDescriptor(UIIcons.OVR_STAGED_ADD);
245 stagedRemovedImage = new CachedImageDescriptor(
246 UIIcons.OVR_STAGED_REMOVE);
247 conflictImage = new CachedImageDescriptor(UIIcons.OVR_CONFLICT);
248 assumeValidImage = new CachedImageDescriptor(UIIcons.OVR_ASSUMEVALID);
252 * Constructs a decorator using the rules from the given
253 * <code>preferencesStore</code>
255 * @param preferencesStore
256 * the preferences store with the preferred decorator rules
258 public DecorationHelper(IPreferenceStore preferencesStore) {
259 store = preferencesStore;
263 * Decorates the given <code>decoration</code> based on the state of the
264 * given <code>resource</code>, using the preferences passed when
265 * constructing this decoration helper.
267 * @param decoration
268 * the decoration to decorate
269 * @param resource
270 * the resource to retrieve state from
272 public void decorate(IDecoration decoration,
273 IDecoratableResource resource) {
274 if (resource.isIgnored())
275 return;
277 decorateText(decoration, resource);
278 decorateIcons(decoration, resource);
281 private void decorateText(IDecoration decoration,
282 IDecoratableResource resource) {
283 String format = "";
284 switch (resource.getType()) {
285 case IResource.FILE:
286 format = store
287 .getString(UIPreferences.DECORATOR_FILETEXT_DECORATION);
288 break;
289 case IResource.FOLDER:
290 format = store
291 .getString(UIPreferences.DECORATOR_FOLDERTEXT_DECORATION);
292 break;
293 case IResource.PROJECT:
294 format = store
295 .getString(UIPreferences.DECORATOR_PROJECTTEXT_DECORATION);
296 break;
299 Map<String, String> bindings = new HashMap<String, String>();
300 bindings.put(BINDING_RESOURCE_NAME, resource.getName());
301 bindings.put(BINDING_BRANCH_NAME, resource.getBranch());
302 bindings.put(BINDING_DIRTY_FLAG, resource.isDirty() ? ">" : null);
303 bindings.put(BINDING_STAGED_FLAG,
304 resource.staged() != Staged.NOT_STAGED ? "*" : null);
306 decorate(decoration, format, bindings);
309 private void decorateIcons(IDecoration decoration,
310 IDecoratableResource resource) {
311 ImageDescriptor overlay = null;
313 if (resource.isTracked()) {
314 if (store.getBoolean(UIPreferences.DECORATOR_SHOW_TRACKED_ICON))
315 overlay = trackedImage;
317 if (store
318 .getBoolean(UIPreferences.DECORATOR_SHOW_ASSUME_VALID_ICON)
319 && resource.isAssumeValid())
320 overlay = assumeValidImage;
322 // Staged overrides tracked
323 Staged staged = resource.staged();
324 if (store.getBoolean(UIPreferences.DECORATOR_SHOW_STAGED_ICON)
325 && staged != Staged.NOT_STAGED) {
326 if (staged == Staged.ADDED)
327 overlay = stagedAddedImage;
328 else if (staged == Staged.REMOVED)
329 overlay = stagedRemovedImage;
330 else
331 overlay = stagedImage;
334 // Conflicts override everything
335 if (store
336 .getBoolean(UIPreferences.DECORATOR_SHOW_CONFLICTS_ICON)
337 && resource.hasConflicts())
338 overlay = conflictImage;
340 } else if (store
341 .getBoolean(UIPreferences.DECORATOR_SHOW_UNTRACKED_ICON)) {
342 overlay = untrackedImage;
345 // Overlays can only be added once, so do it at the end
346 decoration.addOverlay(overlay);
350 * Decorates the given <code>decoration</code>, using the specified text
351 * <code>format</code>, and mapped using the variable bindings from
352 * <code>bindings</code>
354 * @param decoration
355 * the decoration to decorate
356 * @param format
357 * the format to base the decoration on
358 * @param bindings
359 * the bindings between variables in the format and actual
360 * values
362 public static void decorate(IDecoration decoration, String format,
363 Map<String, String> bindings) {
364 StringBuffer prefix = new StringBuffer();
365 StringBuffer suffix = new StringBuffer();
366 StringBuffer output = prefix;
368 int length = format.length();
369 int start = -1;
370 int end = length;
371 while (true) {
372 if ((end = format.indexOf('{', start)) > -1) {
373 output.append(format.substring(start + 1, end));
374 if ((start = format.indexOf('}', end)) > -1) {
375 String key = format.substring(end + 1, start);
376 String s;
378 // Allow users to override the binding
379 if (key.indexOf(':') > -1) {
380 String[] keyAndBinding = key.split(":", 2);
381 key = keyAndBinding[0];
382 if (keyAndBinding.length > 1
383 && bindings.get(key) != null)
384 bindings.put(key, keyAndBinding[1]);
387 // We use the BINDING_RESOURCE_NAME key to determine if
388 // we are doing the prefix or suffix. The name isn't
389 // actually part of either.
390 if (key.equals(BINDING_RESOURCE_NAME)) {
391 output = suffix;
392 s = null;
393 } else {
394 s = bindings.get(key);
397 if (s != null) {
398 output.append(s);
399 } else {
400 // Support removing prefix character if binding is
401 // null
402 int curLength = output.length();
403 if (curLength > 0) {
404 char c = output.charAt(curLength - 1);
405 if (c == ':' || c == '@') {
406 output.deleteCharAt(curLength - 1);
410 } else {
411 output.append(format.substring(end, length));
412 break;
414 } else {
415 output.append(format.substring(start + 1, length));
416 break;
420 String prefixString = prefix.toString().replaceAll("^\\s+", "");
421 if (prefixString != null) {
422 decoration.addPrefix(TextProcessor.process(prefixString,
423 "()[].")); //$NON-NLS-1$
425 String suffixString = suffix.toString().replaceAll("\\s+$", "");
426 if (suffixString != null) {
427 decoration.addSuffix(TextProcessor.process(suffixString,
428 "()[].")); //$NON-NLS-1$
433 // -------- Refresh handling --------
436 * Perform a blanket refresh of all decorations
438 public static void refresh() {
439 Display.getDefault().asyncExec(new Runnable() {
440 public void run() {
441 Activator.getDefault().getWorkbench().getDecoratorManager()
442 .update(DECORATOR_ID);
448 * Callback for IPropertyChangeListener events
450 * If any of the relevant preferences has been changed we refresh all
451 * decorations (all projects and their resources).
453 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
455 public void propertyChange(PropertyChangeEvent event) {
456 final String prop = event.getProperty();
457 // If the property is of any interest to us
458 if (prop.equals(TeamUI.GLOBAL_IGNORES_CHANGED)
459 || prop.equals(TeamUI.GLOBAL_FILE_TYPES_CHANGED)
460 || prop.equals(Activator.DECORATORS_CHANGED)) {
461 postLabelEvent(new LabelProviderChangedEvent(this));
466 * Callback for IResourceChangeListener events
468 * Schedules a refresh of the changed resource
470 * If the preference for computing deep dirty states has been set we walk
471 * the ancestor tree of the changed resource and update all parents as well.
473 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
475 public void resourceChanged(IResourceChangeEvent event) {
476 final Set<IResource> resourcesToUpdate = new HashSet<IResource>();
478 try { // Compute the changed resources by looking at the delta
479 event.getDelta().accept(new IResourceDeltaVisitor() {
480 public boolean visit(IResourceDelta delta) throws CoreException {
482 // If the file has changed but not in a way that we care
483 // about (e.g. marker changes to files) then ignore
484 if (delta.getKind() == IResourceDelta.CHANGED
485 && (delta.getFlags() & INTERESTING_CHANGES) == 0) {
486 return true;
489 final IResource resource = delta.getResource();
491 // If the resource is not part of a project under Git
492 // revision control
493 final RepositoryMapping mapping = RepositoryMapping
494 .getMapping(resource);
495 if (mapping == null) {
496 // Ignore the change
497 return true;
500 if (resource.getType() == IResource.ROOT) {
501 // Continue with the delta
502 return true;
505 if (resource.getType() == IResource.PROJECT) {
506 // If the project is not accessible, don't process it
507 if (!resource.isAccessible())
508 return false;
511 // All seems good, schedule the resource for update
512 resourcesToUpdate.add(resource);
514 if (delta.getKind() == IResourceDelta.CHANGED
515 && (delta.getFlags() & IResourceDelta.OPEN) > 1)
516 return false; // Don't recurse when opening projects
517 else
518 return true;
520 }, true /* includePhantoms */);
521 } catch (final CoreException e) {
522 handleException(null, e);
525 if (resourcesToUpdate.isEmpty())
526 return;
528 // If ancestor-decoration is enabled in the preferences we walk
529 // the ancestor tree of each of the changed resources and add
530 // their parents to the update set
531 final IPreferenceStore store = Activator.getDefault()
532 .getPreferenceStore();
533 if (store.getBoolean(UIPreferences.DECORATOR_RECOMPUTE_ANCESTORS)) {
534 final IResource[] changedResources = resourcesToUpdate
535 .toArray(new IResource[resourcesToUpdate.size()]);
536 for (IResource current : changedResources) {
537 while (current.getType() != IResource.ROOT) {
538 current = current.getParent();
539 resourcesToUpdate.add(current);
544 postLabelEvent(new LabelProviderChangedEvent(this, resourcesToUpdate
545 .toArray()));
549 * Callback for RepositoryListener events
551 * We resolve the repository mapping for the changed repository and forward
552 * that to repositoryChanged(RepositoryMapping).
554 * @param e
555 * The original change event
557 private void repositoryChanged(RepositoryChangedEvent e) {
558 final Set<RepositoryMapping> ms = new HashSet<RepositoryMapping>();
559 for (final IProject p : ResourcesPlugin.getWorkspace().getRoot()
560 .getProjects()) {
561 final RepositoryMapping mapping = RepositoryMapping.getMapping(p);
562 if (mapping != null && mapping.getRepository() == e.getRepository())
563 ms.add(mapping);
565 for (final RepositoryMapping m : ms) {
566 repositoryChanged(m);
571 * (non-Javadoc)
573 * @see
574 * org.spearce.jgit.lib.RepositoryListener#indexChanged(org.spearce.jgit
575 * .lib.IndexChangedEvent)
577 public void indexChanged(IndexChangedEvent e) {
578 repositoryChanged(e);
582 * (non-Javadoc)
584 * @see
585 * org.spearce.jgit.lib.RepositoryListener#refsChanged(org.spearce.jgit.
586 * lib.RefsChangedEvent)
588 public void refsChanged(RefsChangedEvent e) {
589 repositoryChanged(e);
593 * Callback for RepositoryChangeListener events, as well as
594 * RepositoryListener events via repositoryChanged()
596 * @see org.eclipse.egit.core.project.RepositoryChangeListener#repositoryChanged(org.eclipse.egit.core.project.RepositoryMapping)
598 public void repositoryChanged(RepositoryMapping mapping) {
599 // Until we find a way to refresh visible labels within a project
600 // we have to use this blanket refresh that includes all projects.
601 postLabelEvent(new LabelProviderChangedEvent(this));
604 // -------- Helper methods --------
606 private static IResource getResource(Object element) {
607 if (element instanceof ResourceMapping) {
608 element = ((ResourceMapping) element).getModelObject();
611 IResource resource = null;
612 if (element instanceof IResource) {
613 resource = (IResource) element;
614 } else if (element instanceof IAdaptable) {
615 final IAdaptable adaptable = (IAdaptable) element;
616 resource = (IResource) adaptable.getAdapter(IResource.class);
617 if (resource == null) {
618 final IContributorResourceAdapter adapter = (IContributorResourceAdapter) adaptable
619 .getAdapter(IContributorResourceAdapter.class);
620 if (adapter != null)
621 resource = adapter.getAdaptedResource(adaptable);
625 return resource;
629 * Post the label event to the UI thread
631 * @param event
632 * The event to post
634 private void postLabelEvent(final LabelProviderChangedEvent event) {
635 Display.getDefault().asyncExec(new Runnable() {
636 public void run() {
637 fireLabelProviderChanged(event);
643 * Handle exceptions that occur in the decorator. Exceptions are only logged
644 * for resources that are accessible (i.e. exist in an open project).
646 * @param resource
647 * The resource that triggered the exception
648 * @param e
649 * The exception that occurred
651 private static void handleException(IResource resource, CoreException e) {
652 if (resource == null || resource.isAccessible())
653 exceptions.handleException(e);