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
;
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
;
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
73 * TODO: Add support for colors and font decoration
75 public class GitLightweightDecorator
extends LabelProvider
implements
76 ILightweightLabelDecorator
, IPropertyChangeListener
,
77 IResourceChangeListener
, RepositoryChangeListener
, RepositoryListener
{
80 * Property constant pointing back to the extension point id of the
83 public static final String DECORATOR_ID
= "org.eclipse.egit.ui.internal.decorators.GitLightweightDecorator"; //$NON-NLS-1$
86 * Bit-mask describing interesting changes for IResourceChangeListener
89 private static int INTERESTING_CHANGES
= IResourceDelta
.CONTENT
90 | IResourceDelta
.MOVED_FROM
| IResourceDelta
.MOVED_TO
91 | IResourceDelta
.OPEN
| IResourceDelta
.REPLACED
92 | IResourceDelta
.TYPE
;
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
);
118 * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
121 public void 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)
143 // Don't decorate if the workbench is not running
144 if (!PlatformUI
.isWorkbenchRunning())
147 // Don't decorate if UI plugin is not running
148 Activator activator
= Activator
.getDefault();
149 if (activator
== null)
152 // Don't decorate the workspace root
153 if (resource
.getType() == IResource
.ROOT
)
156 // Don't decorate non-existing resources
157 if (!resource
.exists() && !resource
.isPhantom())
160 // Make sure we're dealing with a project under Git revision control
161 final RepositoryMapping mapping
= RepositoryMapping
162 .getMapping(resource
);
166 // Cannot decorate linked resources
167 if (mapping
.getRepoRelativePath(resource
) == null)
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
185 * Used for real-time decoration, as well as in the decorator preview
188 public static class DecorationHelper
{
191 public static final String BINDING_RESOURCE_NAME
= "name"; //$NON-NLS-1$
194 public static final String BINDING_BRANCH_NAME
= "branch"; //$NON-NLS-1$
197 public static final String BINDING_DIRTY_FLAG
= "dirty"; //$NON-NLS-1$
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
208 private static class CachedImageDescriptor
extends ImageDescriptor
{
209 ImageDescriptor descriptor
;
213 public CachedImageDescriptor(ImageDescriptor descriptor
) {
214 this.descriptor
= descriptor
;
217 public ImageData
getImageData() {
219 data
= descriptor
.getImageData();
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
;
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.
268 * the decoration to decorate
270 * the resource to retrieve state from
272 public void decorate(IDecoration decoration
,
273 IDecoratableResource resource
) {
274 if (resource
.isIgnored())
277 decorateText(decoration
, resource
);
278 decorateIcons(decoration
, resource
);
281 private void decorateText(IDecoration decoration
,
282 IDecoratableResource resource
) {
284 switch (resource
.getType()) {
287 .getString(UIPreferences
.DECORATOR_FILETEXT_DECORATION
);
289 case IResource
.FOLDER
:
291 .getString(UIPreferences
.DECORATOR_FOLDERTEXT_DECORATION
);
293 case IResource
.PROJECT
:
295 .getString(UIPreferences
.DECORATOR_PROJECTTEXT_DECORATION
);
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
;
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
;
331 overlay
= stagedImage
;
334 // Conflicts override everything
336 .getBoolean(UIPreferences
.DECORATOR_SHOW_CONFLICTS_ICON
)
337 && resource
.hasConflicts())
338 overlay
= conflictImage
;
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>
355 * the decoration to decorate
357 * the format to base the decoration on
359 * the bindings between variables in the format and actual
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();
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
);
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
)) {
394 s
= bindings
.get(key
);
400 // Support removing prefix character if binding is
402 int curLength
= output
.length();
404 char c
= output
.charAt(curLength
- 1);
405 if (c
== ':' || c
== '@') {
406 output
.deleteCharAt(curLength
- 1);
411 output
.append(format
.substring(end
, length
));
415 output
.append(format
.substring(start
+ 1, length
));
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() {
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) {
489 final IResource resource
= delta
.getResource();
491 // If the resource is not part of a project under Git
493 final RepositoryMapping mapping
= RepositoryMapping
494 .getMapping(resource
);
495 if (mapping
== null) {
500 if (resource
.getType() == IResource
.ROOT
) {
501 // Continue with the delta
505 if (resource
.getType() == IResource
.PROJECT
) {
506 // If the project is not accessible, don't process it
507 if (!resource
.isAccessible())
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
520 }, true /* includePhantoms */);
521 } catch (final CoreException e
) {
522 handleException(null, e
);
525 if (resourcesToUpdate
.isEmpty())
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
549 * Callback for RepositoryListener events
551 * We resolve the repository mapping for the changed repository and forward
552 * that to repositoryChanged(RepositoryMapping).
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()
561 final RepositoryMapping mapping
= RepositoryMapping
.getMapping(p
);
562 if (mapping
!= null && mapping
.getRepository() == e
.getRepository())
565 for (final RepositoryMapping m
: ms
) {
566 repositoryChanged(m
);
574 * org.spearce.jgit.lib.RepositoryListener#indexChanged(org.spearce.jgit
575 * .lib.IndexChangedEvent)
577 public void indexChanged(IndexChangedEvent e
) {
578 repositoryChanged(e
);
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);
621 resource
= adapter
.getAdaptedResource(adaptable
);
629 * Post the label event to the UI thread
634 private void postLabelEvent(final LabelProviderChangedEvent event
) {
635 Display
.getDefault().asyncExec(new Runnable() {
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).
647 * The resource that triggered the exception
649 * The exception that occurred
651 private static void handleException(IResource resource
, CoreException e
) {
652 if (resource
== null || resource
.isAccessible())
653 exceptions
.handleException(e
);