Better align repository hyperlink in commit viewer
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / Activator.java
blob13c62155a444245fb4dd2fff2c04c628a486722e
1 /*******************************************************************************
2 * Copyright (C) 2007,2010 Robin Rosenberg <robin.rosenberg@dewire.com>
3 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
5 * Copyright (C) 2012, Matthias Sohn <matthias.sohn@sap.com>
6 * Copyright (C) 2015, Philipp Bumann <bumannp@gmail.com>
7 * Copyright (C) 2016, Dani Megert <daniel_megert@ch.ibm.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 *******************************************************************************/
14 package org.eclipse.egit.ui;
16 import java.io.IOException;
17 import java.lang.reflect.InvocationTargetException;
18 import java.util.ArrayList;
19 import java.util.Dictionary;
20 import java.util.Hashtable;
21 import java.util.Iterator;
22 import java.util.LinkedHashSet;
23 import java.util.List;
24 import java.util.Set;
26 import org.eclipse.core.resources.IProject;
27 import org.eclipse.core.resources.IResource;
28 import org.eclipse.core.resources.IWorkspace;
29 import org.eclipse.core.resources.IWorkspaceRunnable;
30 import org.eclipse.core.resources.ResourcesPlugin;
31 import org.eclipse.core.runtime.CoreException;
32 import org.eclipse.core.runtime.IProgressMonitor;
33 import org.eclipse.core.runtime.IStatus;
34 import org.eclipse.core.runtime.Status;
35 import org.eclipse.core.runtime.SubMonitor;
36 import org.eclipse.core.runtime.jobs.ISchedulingRule;
37 import org.eclipse.core.runtime.jobs.Job;
38 import org.eclipse.egit.core.RepositoryCache;
39 import org.eclipse.egit.core.RepositoryUtil;
40 import org.eclipse.egit.core.project.RepositoryMapping;
41 import org.eclipse.egit.ui.internal.ConfigurationChecker;
42 import org.eclipse.egit.ui.internal.KnownHosts;
43 import org.eclipse.egit.ui.internal.RepositoryCacheRule;
44 import org.eclipse.egit.ui.internal.UIText;
45 import org.eclipse.egit.ui.internal.credentials.EGitCredentialsProvider;
46 import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
47 import org.eclipse.egit.ui.internal.variables.GitTemplateVariableResolver;
48 import org.eclipse.jdt.internal.ui.JavaPlugin;
49 import org.eclipse.jface.resource.JFaceResources;
50 import org.eclipse.jface.resource.LocalResourceManager;
51 import org.eclipse.jface.resource.ResourceManager;
52 import org.eclipse.jface.text.templates.ContextTypeRegistry;
53 import org.eclipse.jface.text.templates.TemplateContextType;
54 import org.eclipse.jface.util.IPropertyChangeListener;
55 import org.eclipse.jface.util.PropertyChangeEvent;
56 import org.eclipse.jgit.events.IndexChangedEvent;
57 import org.eclipse.jgit.events.IndexChangedListener;
58 import org.eclipse.jgit.events.ListenerHandle;
59 import org.eclipse.jgit.events.RepositoryEvent;
60 import org.eclipse.jgit.lib.Repository;
61 import org.eclipse.jgit.transport.CredentialsProvider;
62 import org.eclipse.osgi.service.debug.DebugOptions;
63 import org.eclipse.osgi.service.debug.DebugOptionsListener;
64 import org.eclipse.swt.graphics.Font;
65 import org.eclipse.swt.widgets.Display;
66 import org.eclipse.ui.IWindowListener;
67 import org.eclipse.ui.IWorkbenchWindow;
68 import org.eclipse.ui.PlatformUI;
69 import org.eclipse.ui.plugin.AbstractUIPlugin;
70 import org.eclipse.ui.statushandlers.StatusManager;
71 import org.eclipse.ui.themes.ITheme;
72 import org.osgi.framework.BundleContext;
74 /**
75 * This is a plugin singleton mostly controlling logging.
77 public class Activator extends AbstractUIPlugin implements DebugOptionsListener {
79 /**
80 * The one and only instance
82 private static Activator plugin;
84 /**
85 * Property listeners for plugin specific events
87 private static List<IPropertyChangeListener> propertyChangeListeners =
88 new ArrayList<>(5);
90 /**
91 * Property constant indicating the decorator configuration has changed.
93 public static final String DECORATORS_CHANGED = "org.eclipse.egit.ui.DECORATORS_CHANGED"; //$NON-NLS-1$
95 /**
96 * @return the {@link Activator} singleton.
98 public static Activator getDefault() {
99 return plugin;
103 * @return the id of the egit ui plugin
105 public static String getPluginId() {
106 return getDefault().getBundle().getSymbolicName();
110 * Creates an {@link IStatus} from the parameters. If the throwable is an
111 * {@link InvocationTargetException}, the status is created from the first
112 * exception that is either not an InvocationTargetException or that has a
113 * message. If the message passed is empty, tries to supply a message from
114 * that exception.
116 * @param severity
117 * of the {@link IStatus}
118 * @param message
119 * for the status
120 * @param throwable
121 * that caused the status, may be {@code null}
122 * @return the status
124 private static IStatus toStatus(int severity, String message,
125 Throwable throwable) {
126 Throwable exc = throwable;
127 while (exc instanceof InvocationTargetException) {
128 String msg = exc.getLocalizedMessage();
129 if (msg != null && !msg.isEmpty()) {
130 break;
132 Throwable cause = exc.getCause();
133 if (cause == null) {
134 break;
136 exc = cause;
138 if (exc != null && (message == null || message.isEmpty())) {
139 message = exc.getLocalizedMessage();
141 return new Status(severity, getPluginId(), message, exc);
145 * Handle an error. The error is logged. If <code>show</code> is
146 * <code>true</code> the error is shown to the user.
148 * @param message
149 * a localized message
150 * @param throwable
151 * @param show
153 public static void handleError(String message, Throwable throwable,
154 boolean show) {
155 handleIssue(IStatus.ERROR, message, throwable, show);
159 * Handle an issue. The issue is logged. If <code>show</code> is
160 * <code>true</code> the issue is shown to the user.
162 * @param severity
163 * status severity, use constants defined in {@link IStatus}
164 * @param message
165 * a localized message
166 * @param throwable
167 * @param show
168 * @since 2.2
170 public static void handleIssue(int severity, String message, Throwable throwable,
171 boolean show) {
172 IStatus status = toStatus(severity, message, throwable);
173 int style = StatusManager.LOG;
174 if (show)
175 style |= StatusManager.SHOW;
176 StatusManager.getManager().handle(status, style);
180 * Shows an error. The error is NOT logged.
182 * @param message
183 * a localized message
184 * @param throwable
186 public static void showError(String message, Throwable throwable) {
187 IStatus status = toStatus(IStatus.ERROR, message, throwable);
188 StatusManager.getManager().handle(status, StatusManager.SHOW);
192 * Shows an error. The error is NOT logged.
194 * @param message
195 * a localized message
196 * @param status
198 public static void showErrorStatus(String message, IStatus status) {
199 StatusManager.getManager().handle(status, StatusManager.SHOW);
203 * @param message
204 * @param e
206 public static void logError(String message, Throwable e) {
207 handleError(message, e, false);
211 * Utility method to log warnings for this plug-in.
213 * @param message
214 * User comprehensible message
215 * @param thr
216 * The exception through which we noticed the warning
218 public static void logWarning(final String message, final Throwable thr) {
219 handleIssue(IStatus.WARNING, message, thr, false);
223 * @param message
224 * @param e
226 public static void error(String message, Throwable e) {
227 handleError(message, e, false);
231 * Creates an error status
233 * @param message
234 * a localized message
235 * @param throwable
236 * @return a new Status object
238 public static IStatus createErrorStatus(String message,
239 Throwable throwable) {
240 return toStatus(IStatus.ERROR, message, throwable);
244 * Creates an error status
246 * @param message
247 * a localized message
248 * @return a new Status object
250 public static IStatus createErrorStatus(String message) {
251 return toStatus(IStatus.ERROR, message, null);
255 * Get the theme used by this plugin.
257 * @return our theme.
259 public static ITheme getTheme() {
260 return plugin.getWorkbench().getThemeManager().getCurrentTheme();
264 * Get a font known to this plugin.
266 * @param id
267 * one of our THEME_* font preference ids (see
268 * {@link UIPreferences});
269 * @return the configured font, borrowed from the registry.
271 public static Font getFont(final String id) {
272 return getTheme().getFontRegistry().get(id);
276 * Get a font known to this plugin, but with bold style applied over top.
278 * @param id
279 * one of our THEME_* font preference ids (see
280 * {@link UIPreferences});
281 * @return the configured font, borrowed from the registry.
283 public static Font getBoldFont(final String id) {
284 return getTheme().getFontRegistry().getBold(id);
287 private ResourceManager resourceManager;
288 private RepositoryChangeScanner rcs;
289 private ResourceRefreshJob refreshJob;
290 private ListenerHandle refreshHandle;
291 private DebugOptions debugOptions;
293 private volatile boolean uiIsActive;
294 private IWindowListener focusListener;
297 * Construct the {@link Activator} egit ui plugin singleton instance
299 public Activator() {
300 Activator.setActivator(this);
303 private static void setActivator(Activator a) {
304 plugin = a;
308 @Override
309 public void start(final BundleContext context) throws Exception {
310 super.start(context);
311 // we want to be notified about debug options changes
312 Dictionary<String, String> props = new Hashtable<>(4);
313 props.put(DebugOptions.LISTENER_SYMBOLICNAME, context.getBundle()
314 .getSymbolicName());
315 context.registerService(DebugOptionsListener.class.getName(), this,
316 props);
318 setupRepoChangeScanner();
319 setupRepoIndexRefresh();
320 setupFocusHandling();
321 setupCredentialsProvider();
322 ConfigurationChecker.checkConfiguration();
324 registerTemplateVariableResolvers();
327 private void setupCredentialsProvider() {
328 CredentialsProvider.setDefault(new EGitCredentialsProvider());
331 private void registerTemplateVariableResolvers() {
332 if (hasJavaPlugin()) {
333 final ContextTypeRegistry codeTemplateContextRegistry = JavaPlugin
334 .getDefault().getCodeTemplateContextRegistry();
335 final Iterator<?> ctIter = codeTemplateContextRegistry
336 .contextTypes();
338 while (ctIter.hasNext()) {
339 final TemplateContextType contextType = (TemplateContextType) ctIter
340 .next();
341 contextType
342 .addResolver(new GitTemplateVariableResolver(
343 "git_config", //$NON-NLS-1$
344 UIText.GitTemplateVariableResolver_GitConfigDescription));
350 * @return true if at least one Eclipse window is active
352 static boolean isActive() {
353 return getDefault().uiIsActive;
357 private void setupFocusHandling() {
358 focusListener = new IWindowListener() {
360 private void updateUiState() {
361 Display.getCurrent().asyncExec(new Runnable() {
362 @Override
363 public void run() {
364 boolean wasActive = uiIsActive;
365 uiIsActive = Display.getCurrent().getActiveShell() != null;
366 if (uiIsActive != wasActive
367 && GitTraceLocation.REPOSITORYCHANGESCANNER
368 .isActive())
369 traceUiIsActive();
372 private void traceUiIsActive() {
373 StringBuilder message = new StringBuilder(
374 "workbench is "); //$NON-NLS-1$
375 message.append(uiIsActive ? "active" : "inactive"); //$NON-NLS-1$//$NON-NLS-2$
376 GitTraceLocation.getTrace().trace(
377 GitTraceLocation.REPOSITORYCHANGESCANNER
378 .getLocation(), message.toString());
383 @Override
384 public void windowOpened(IWorkbenchWindow window) {
385 updateUiState();
388 @Override
389 public void windowDeactivated(IWorkbenchWindow window) {
390 updateUiState();
393 @Override
394 public void windowClosed(IWorkbenchWindow window) {
395 updateUiState();
398 @Override
399 public void windowActivated(IWorkbenchWindow window) {
400 updateUiState();
401 // 500: give the UI task a chance to update the active state
402 rcs.schedule(500);
403 refreshJob.triggerRefresh();
406 Job job = new Job(UIText.Activator_setupFocusListener) {
407 @Override
408 protected IStatus run(IProgressMonitor monitor) {
409 if (PlatformUI.isWorkbenchRunning())
410 PlatformUI.getWorkbench().addWindowListener(focusListener);
411 else
412 schedule(1000L);
413 return Status.OK_STATUS;
416 job.schedule();
419 @Override
420 public void optionsChanged(DebugOptions options) {
421 // initialize the trace stuff
422 debugOptions = options;
423 GitTraceLocation.initializeFromOptions(options, isDebugging());
427 * @return the {@link DebugOptions}
429 public DebugOptions getDebugOptions() {
430 return debugOptions;
433 private void setupRepoIndexRefresh() {
434 refreshJob = new ResourceRefreshJob();
435 refreshHandle = Repository.getGlobalListenerList()
436 .addIndexChangedListener(refreshJob);
440 * Register for changes made to Team properties.
442 * @param listener
443 * The listener to register
445 public static synchronized void addPropertyChangeListener(
446 IPropertyChangeListener listener) {
447 propertyChangeListeners.add(listener);
451 * Remove a Team property changes.
453 * @param listener
454 * The listener to remove
456 public static synchronized void removePropertyChangeListener(
457 IPropertyChangeListener listener) {
458 propertyChangeListeners.remove(listener);
462 * Broadcast a Team property change.
464 * @param event
465 * The event to broadcast
467 public static synchronized void broadcastPropertyChange(PropertyChangeEvent event) {
468 for (IPropertyChangeListener listener : propertyChangeListeners)
469 listener.propertyChange(event);
473 * Refresh projects in repositories that we suspect may have resource
474 * changes.
476 static class ResourceRefreshJob extends Job implements
477 IndexChangedListener {
479 ResourceRefreshJob() {
480 super(UIText.Activator_refreshJobName);
481 setUser(false);
482 setSystem(true);
485 private Set<Repository> repositoriesChanged = new LinkedHashSet<>();
487 @Override
488 public IStatus run(IProgressMonitor monitor) {
489 Set<Repository> repos;
490 synchronized (repositoriesChanged) {
491 if (repositoriesChanged.isEmpty()) {
492 return Status.OK_STATUS;
494 repos = new LinkedHashSet<>(repositoriesChanged);
495 repositoriesChanged.clear();
497 IWorkspace workspace = ResourcesPlugin.getWorkspace();
498 IProject[] projects = workspace.getRoot().getProjects();
499 final Set<IProject> toRefresh = new LinkedHashSet<>();
500 for (IProject p : projects) {
501 if (!p.isAccessible()) {
502 continue;
504 RepositoryMapping mapping = RepositoryMapping.getMapping(p);
505 if (mapping != null
506 && repos.contains(mapping.getRepository())) {
507 toRefresh.add(p);
511 if (toRefresh.isEmpty()) {
512 return Status.OK_STATUS;
515 try {
516 workspace.run(new IWorkspaceRunnable() {
517 @Override
518 public void run(IProgressMonitor m) throws CoreException {
519 SubMonitor subMonitor = SubMonitor.convert(m,
520 UIText.Activator_refreshingProjects,
521 toRefresh.size());
522 for (IProject p : toRefresh) {
523 if (subMonitor.isCanceled()) {
524 return;
526 ISchedulingRule rule = p.getWorkspace().getRuleFactory().refreshRule(p);
527 try {
528 getJobManager().beginRule(rule, subMonitor);
529 // handle missing projects after branch switch
530 if (p.isAccessible()) {
531 p.refreshLocal(IResource.DEPTH_INFINITE,
532 subMonitor.newChild(1));
534 } catch (CoreException e) {
535 handleError(UIText.Activator_refreshFailed, e, false);
536 } finally {
537 getJobManager().endRule(rule);
541 }, workspace.getRuleFactory().refreshRule(workspace.getRoot()),
542 IWorkspace.AVOID_UPDATE, monitor);
543 } catch (CoreException e) {
544 handleError(UIText.Activator_refreshFailed, e, false);
545 return new Status(IStatus.ERROR, getPluginId(), e.getMessage());
548 if (!monitor.isCanceled()) {
549 // re-schedule if we got some changes in the meantime
550 synchronized (repositoriesChanged) {
551 if (!repositoriesChanged.isEmpty()) {
552 schedule(100);
556 monitor.done();
557 return Status.OK_STATUS;
560 @Override
561 public void onIndexChanged(IndexChangedEvent e) {
562 if (Activator.getDefault().getPreferenceStore()
563 .getBoolean(UIPreferences.REFESH_ON_INDEX_CHANGE)) {
564 mayTriggerRefresh(e);
569 * Record which projects have changes. Initiate a resource refresh job
570 * if the user settings allow it.
572 * @param e
573 * The {@link RepositoryEvent} that triggered this refresh
575 private void mayTriggerRefresh(RepositoryEvent e) {
576 synchronized (repositoriesChanged) {
577 repositoriesChanged.add(e.getRepository());
579 if (!Activator.getDefault().getPreferenceStore()
580 .getBoolean(UIPreferences.REFESH_ONLY_WHEN_ACTIVE)
581 || isActive()) {
582 triggerRefresh();
587 * Figure which projects belong to a repository, add them to a set of
588 * project to refresh and schedule the refresh as a job.
590 void triggerRefresh() {
591 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
592 GitTraceLocation.getTrace().trace(
593 GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
594 "Triggered refresh"); //$NON-NLS-1$
596 schedule();
601 * A Job that looks at the repository meta data and triggers a refresh of
602 * the resources in the affected projects.
604 private static class RepositoryChangeScanner extends Job
605 implements IPropertyChangeListener {
607 // volatile in order to ensure thread synchronization
608 private volatile boolean doReschedule;
610 private int interval;
612 private final RepositoryCache repositoryCache;
614 RepositoryChangeScanner() {
615 super(UIText.Activator_repoScanJobName);
616 setRule(new RepositoryCacheRule());
617 setSystem(true);
618 setUser(false);
619 repositoryCache = org.eclipse.egit.core.Activator.getDefault()
620 .getRepositoryCache();
621 updateRefreshInterval();
624 @Override
625 public boolean shouldSchedule() {
626 return doReschedule;
629 @Override
630 public boolean shouldRun() {
631 return doReschedule;
634 void setReschedule(boolean reschedule){
635 doReschedule = reschedule;
638 @Override
639 protected IStatus run(IProgressMonitor monitor) {
640 // When people use Git from the command line a lot of changes
641 // may happen. Don't scan when inactive depending on the user's
642 // choice.
643 if (getDefault().getPreferenceStore()
644 .getBoolean(UIPreferences.REFESH_ONLY_WHEN_ACTIVE)) {
645 if (!isActive()) {
646 monitor.done();
647 return Status.OK_STATUS;
651 Repository[] repos = repositoryCache.getAllRepositories();
652 if (repos.length == 0) {
653 return Status.OK_STATUS;
656 monitor.beginTask(UIText.Activator_scanningRepositories,
657 repos.length);
658 try {
659 for (Repository repo : repos) {
660 if (monitor.isCanceled()) {
661 break;
663 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
664 GitTraceLocation.getTrace().trace(
665 GitTraceLocation.REPOSITORYCHANGESCANNER
666 .getLocation(),
667 "Scanning " + repo + " for changes"); //$NON-NLS-1$ //$NON-NLS-2$
670 repo.scanForRepoChanges();
671 monitor.worked(1);
673 } catch (IOException e) {
674 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
675 GitTraceLocation.getTrace().trace(
676 GitTraceLocation.REPOSITORYCHANGESCANNER
677 .getLocation(),
678 "Stopped rescheduling " + getName() + "job"); //$NON-NLS-1$ //$NON-NLS-2$
680 return createErrorStatus(UIText.Activator_scanError, e);
681 } finally {
682 monitor.done();
684 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
685 GitTraceLocation.getTrace().trace(
686 GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
687 "Rescheduling " + getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
689 schedule(interval);
690 return Status.OK_STATUS;
693 @Override
694 public void propertyChange(PropertyChangeEvent event) {
695 if (!UIPreferences.REFESH_INDEX_INTERVAL
696 .equals(event.getProperty())) {
697 return;
699 updateRefreshInterval();
702 private void updateRefreshInterval() {
703 interval = getRefreshIndexInterval();
704 setReschedule(interval > 0);
705 cancel();
706 schedule(interval);
710 * @return interval in milliseconds for automatic index check, 0 is if
711 * check should be disabled
713 private static int getRefreshIndexInterval() {
714 return 1000 * getDefault().getPreferenceStore()
715 .getInt(UIPreferences.REFESH_INDEX_INTERVAL);
719 private void setupRepoChangeScanner() {
720 rcs = new RepositoryChangeScanner();
721 getPreferenceStore().addPropertyChangeListener(rcs);
724 @Override
725 public void stop(final BundleContext context) throws Exception {
726 if (refreshHandle != null) {
727 refreshHandle.remove();
728 refreshHandle = null;
731 if (focusListener != null) {
732 if (PlatformUI.isWorkbenchRunning()) {
733 PlatformUI.getWorkbench().removeWindowListener(focusListener);
735 focusListener = null;
738 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
739 GitTraceLocation.getTrace().trace(
740 GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
741 "Trying to cancel " + rcs.getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
744 getPreferenceStore().removePropertyChangeListener(rcs);
745 rcs.setReschedule(false);
746 rcs.cancel();
747 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
748 GitTraceLocation.getTrace().trace(
749 GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
750 "Trying to cancel " + refreshJob.getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
752 refreshJob.cancel();
754 rcs.join();
755 refreshJob.join();
757 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
758 GitTraceLocation.getTrace().trace(
759 GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
760 "Jobs terminated"); //$NON-NLS-1$
762 if (resourceManager != null) {
763 resourceManager.dispose();
764 resourceManager = null;
766 super.stop(context);
767 plugin = null;
770 @Override
771 protected void saveDialogSettings() {
772 KnownHosts.store();
773 super.saveDialogSettings();
776 * @return the {@link RepositoryUtil} instance
778 public RepositoryUtil getRepositoryUtil() {
779 return org.eclipse.egit.core.Activator.getDefault().getRepositoryUtil();
783 * Gets this plugin's {@link ResourceManager}.
785 * @return the {@link ResourceManager} of this plugin
787 public synchronized ResourceManager getResourceManager() {
788 if (resourceManager == null) {
789 Display display = PlatformUI.getWorkbench().getDisplay();
790 if (display == null) {
791 // Workbench already closed?
792 throw new IllegalStateException();
794 resourceManager = new LocalResourceManager(JFaceResources
795 .getResources(display));
797 return resourceManager;
801 * @return true if the Java Plugin is loaded
803 public static final boolean hasJavaPlugin() {
804 try {
805 return Class.forName("org.eclipse.jdt.internal.ui.JavaPlugin") != null; //$NON-NLS-1$
806 } catch (ClassNotFoundException e) {
807 return false;