/******************************************************************************* * Copyright (C) 2015, 2016 Thomas Wolf <thomas.wolf@paranor.ch>. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.egit.ui.internal.dialogs; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import org.eclipse.jface.action.Action; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.hyperlink.IHyperlink; import org.eclipse.jface.text.hyperlink.IHyperlinkDetector; import org.eclipse.jface.text.hyperlink.IHyperlinkDetectorExtension; import org.eclipse.jface.text.hyperlink.IHyperlinkDetectorExtension2; import org.eclipse.jface.text.source.IOverviewRuler; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.IVerticalRuler; import org.eclipse.jface.text.source.SourceViewerConfiguration; import org.eclipse.jface.text.source.projection.ProjectionViewer; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.ui.editors.text.EditorsUI; import org.eclipse.ui.editors.text.TextSourceViewerConfiguration; import org.eclipse.ui.texteditor.AbstractTextEditor; import org.eclipse.ui.texteditor.HyperlinkDetectorDescriptor; import org.eclipse.ui.texteditor.spelling.SpellingProblem; import org.eclipse.ui.texteditor.spelling.SpellingService; /** * A {@link ProjectionViewer} that automatically reacts to changes in the * hyperlinking and spellchecking preferences. */ public class HyperlinkSourceViewer extends ProjectionViewer { // The default SourceViewer doesn't do this and instead AbstractTextEditor // has code that does all that. For our uses it is much more convenient if // the viewer itself handles this. // // Note: although ProjectionViewer is marked as noextend, there are already // a number of subclasses. private Configuration configuration; private Set<String> preferenceKeysForEnablement; private Set<String> preferenceKeysForActivation; private IPropertyChangeListener hyperlinkChangeListener; private IPropertyChangeListener spellCheckingListener; /** * Constructs a new source viewer. The vertical ruler is initially visible. * The viewer has not yet been initialized with a source viewer * configuration. * * @param parent * the parent of the viewer's control * @param ruler * the vertical ruler used by this source viewer * @param styles * the SWT style bits for the viewer's control, */ public HyperlinkSourceViewer(Composite parent, IVerticalRuler ruler, int styles) { this(parent, ruler, null, false, styles); } /** * Constructs a new source viewer. The vertical ruler is initially visible. * The overview ruler visibility is controlled by the value of * <code>showAnnotationsOverview</code>. The viewer has not yet been * initialized with a source viewer configuration. * * @param parent * the parent of the viewer's control * @param verticalRuler * the vertical ruler used by this source viewer * @param overviewRuler * the overview ruler * @param showAnnotationsOverview * {@code true} if the overview ruler should be visible, * {@code false} otherwise * @param styles * the SWT style bits for the viewer's control, */ public HyperlinkSourceViewer(Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler, boolean showAnnotationsOverview, int styles) { super(parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles); } @Override public void configure(SourceViewerConfiguration config) { super.configure(config); if (config instanceof Configuration) { configuration = (Configuration) config; configurePreferenceKeys(); // Install a listener hyperlinkChangeListener = new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { String property = event.getProperty(); if (preferenceKeysForEnablement.contains(property)) { resetHyperlinkDetectors(); final Control control = getControl(); if (control != null && !control.isDisposed()) { control.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (!control.isDisposed()) { refresh(); } } }); } } else if (preferenceKeysForActivation.contains(property)) { resetHyperlinkDetectors(); } } }; EditorsUI.getPreferenceStore() .addPropertyChangeListener(hyperlinkChangeListener); } else { configuration = null; hyperlinkChangeListener = null; } spellCheckingListener = event -> { if (SpellingService.PREFERENCE_SPELLING_ENABLED .equals(event.getProperty())) { boolean isEnabled = EditorsUI.getPreferenceStore().getBoolean( SpellingService.PREFERENCE_SPELLING_ENABLED); updateSpellChecking(isEnabled); } }; EditorsUI.getPreferenceStore() .addPropertyChangeListener(spellCheckingListener); this.getTextWidget().addDisposeListener(event -> { if (hyperlinkChangeListener != null) { EditorsUI.getPreferenceStore() .removePropertyChangeListener(hyperlinkChangeListener); } EditorsUI.getPreferenceStore() .removePropertyChangeListener(spellCheckingListener); }); } private void updateSpellChecking(boolean isEnabled) { // See TextEditor.handlePreferenceStoreChanged. this.unconfigure(); this.configure(configuration); if (!isEnabled) { SpellingProblem.removeAll(this, null); } } private void configurePreferenceKeys() { preferenceKeysForEnablement = new HashSet<>(); preferenceKeysForActivation = new HashSet<>(); // Global settings (master switch) preferenceKeysForEnablement .add(AbstractTextEditor.PREFERENCE_HYPERLINKS_ENABLED); preferenceKeysForActivation .add(AbstractTextEditor.PREFERENCE_HYPERLINK_KEY_MODIFIER); // All applicable individual hyperlink detectors settings. Set<?> targets = configuration.getHyperlinkDetectorTargets(this) .keySet(); for (HyperlinkDetectorDescriptor desc : EditorsUI .getHyperlinkDetectorRegistry() .getHyperlinkDetectorDescriptors()) { if (targets.contains(desc.getTargetId())) { preferenceKeysForEnablement.add(desc.getId()); preferenceKeysForActivation.add(desc.getId() + HyperlinkDetectorDescriptor.STATE_MASK_POSTFIX); } } } private void resetHyperlinkDetectors() { IHyperlinkDetector[] detectors = configuration .getHyperlinkDetectors(this); int stateMask = configuration.getHyperlinkStateMask(this); setHyperlinkDetectors(detectors, stateMask); } @Override public void unconfigure() { super.unconfigure(); if (hyperlinkChangeListener != null) { EditorsUI.getPreferenceStore() .removePropertyChangeListener(hyperlinkChangeListener); } if (spellCheckingListener != null) { EditorsUI.getPreferenceStore() .removePropertyChangeListener(spellCheckingListener); } preferenceKeysForEnablement = null; preferenceKeysForActivation = null; } /** * A {@link TextSourceViewerConfiguration} for use in conjunction with * {@link HyperlinkSourceViewer}. Includes support for opening hyperlinks on * the configured modifier key, or also when no key is pressed (i.e., on * {@link SWT#NONE}) if * {@link TextSourceViewerConfiguration#getHyperlinkStateMask(ISourceViewer) * getHyperlinkStateMask(ISourceViewer)} returns {@link SWT#NONE} */ public static class Configuration extends TextSourceViewerConfiguration { /** * Creates a new configuration. */ public Configuration() { super(); } /** * Creates a new configuration and initializes it with the given * preference store. * * @param preferenceStore * the preference store used to initialize this configuration */ public Configuration(IPreferenceStore preferenceStore) { super(preferenceStore); } /** * Returns the hyperlink detectors which are to be used to detect * hyperlinks in the given source viewer. This implementation uses * {@link #internalGetHyperlinkDetectors(ISourceViewer)} to get the * hyperlink detectors. * <p> * Sets up the hyperlink detectors such that they are active on both * {@link SWT#NONE} and on the configured modifier key combination if * the viewer is configured to open hyperlinks on direct click, i.e., if * {@link TextSourceViewerConfiguration#getHyperlinkStateMask(ISourceViewer) * getHyperlinkStateMask(ISourceViewer)} returns {@link SWT#NONE}. * </p> * * @param sourceViewer * the {@link ISourceViewer} to be configured by this * configuration * @return an array with {@link IHyperlinkDetector}s or {@code null} if * no hyperlink support should be installed */ @Override public final IHyperlinkDetector[] getHyperlinkDetectors( ISourceViewer sourceViewer) { IHyperlinkDetector[] detectors = internalGetHyperlinkDetectors( sourceViewer); if (detectors != null && detectors.length > 0 && getHyperlinkStateMask(sourceViewer) == SWT.NONE) { // Duplicate them all with a detector that is active on SWT.NONE int defaultMask = getConfiguredDefaultMask(); IHyperlinkDetector[] newDetectors = new IHyperlinkDetector[detectors.length * 2]; int j = 0; for (IHyperlinkDetector original : detectors) { if (original instanceof IHyperlinkDetectorExtension2) { int mask = ((IHyperlinkDetectorExtension2) original) .getStateMask(); if (mask == -1) { newDetectors[j++] = new FixedMaskHyperlinkDetector( original, defaultMask); if (defaultMask != SWT.NONE) { newDetectors[j++] = new NoMaskHyperlinkDetector( original); } } else { newDetectors[j++] = original; if (mask != SWT.NONE) { newDetectors[j++] = new NoMaskHyperlinkDetector( original); } } } else { newDetectors[j++] = original; } } IHyperlinkDetector[] result = new IHyperlinkDetector[j]; System.arraycopy(newDetectors, 0, result, 0, j); return result; } return detectors; } /** * Collects the {@link IHyperlinkDetector}s for the given * {@link ISourceViewer}. * * @param sourceViewer * to get the detectors for * @return the hyperlink detectors, or {@code null} if none shall be * installed * @see TextSourceViewerConfiguration#getHyperlinkDetectors(ISourceViewer) */ protected @Nullable IHyperlinkDetector[] internalGetHyperlinkDetectors( ISourceViewer sourceViewer) { return super.getHyperlinkDetectors(sourceViewer); } @SuppressWarnings("unchecked") @Override protected Map getHyperlinkDetectorTargets(ISourceViewer sourceViewer) { // TODO: use generified signature once EGit's base dependency is // Eclipse 4.5. // Just so that we have visibility on this in the enclosing class. return super.getHyperlinkDetectorTargets(sourceViewer); } } private static abstract class InternalHyperlinkDetector implements IHyperlinkDetector, IHyperlinkDetectorExtension, IHyperlinkDetectorExtension2 { protected IHyperlinkDetector delegate; protected InternalHyperlinkDetector(IHyperlinkDetector delegate) { this.delegate = delegate; } @Override public final IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) { return delegate.detectHyperlinks(textViewer, region, canShowMultipleHyperlinks); } @Override public final void dispose() { if (delegate instanceof IHyperlinkDetectorExtension) { ((IHyperlinkDetectorExtension) delegate).dispose(); } } } private static class FixedMaskHyperlinkDetector extends InternalHyperlinkDetector { private final int mask; protected FixedMaskHyperlinkDetector(IHyperlinkDetector delegate, int mask) { super(delegate); this.mask = mask; } @Override public int getStateMask() { return mask; } } /** * An {@link IHyperlinkDetector} that activates when no modifier key is * pressed. It's protected so that the {@link HyperlinkTokenScanner} can see * it and can avoid calling those for syntax coloring since they are * duplicates of some other hyperlink detector anyway. */ protected static class NoMaskHyperlinkDetector extends FixedMaskHyperlinkDetector { private NoMaskHyperlinkDetector(IHyperlinkDetector delegate) { // Private to allow instantiation only within this compilation unit super(delegate, SWT.NONE); } } private static int getConfiguredDefaultMask() { int mask = computeStateMask(EditorsUI.getPreferenceStore().getString( AbstractTextEditor.PREFERENCE_HYPERLINK_KEY_MODIFIER)); if (mask == -1) { // Fallback mask = EditorsUI.getPreferenceStore().getInt( AbstractTextEditor.PREFERENCE_HYPERLINK_KEY_MODIFIER_MASK); } return mask; } // The preference for // AbstractTextEditor.PREFERENCE_HYPERLINK_KEY_MODIFIER_MASK is not // recomputed when the user changes preferences. Therefore, we have to // use the AbstractTextEditor.PREFERENCE_HYPERLINK_KEY_MODIFIER and // translate that to a state mask explicitly. Code below copied from // org.eclipse.ui.internal.editors.text.HyperlinkDetectorsConfigurationBlock. /** * Maps the localized modifier name to a code in the same manner as * {@link org.eclipse.jface.action.Action#findModifier * Action.findModifier()}. * * @param modifierName * the modifier name * @return the SWT modifier bit, or {@code 0} if no match was found */ private static final int findLocalizedModifier(String modifierName) { if (modifierName == null) { return 0; } if (modifierName .equalsIgnoreCase(Action.findModifierString(SWT.CTRL))) { return SWT.CTRL; } if (modifierName .equalsIgnoreCase(Action.findModifierString(SWT.SHIFT))) { return SWT.SHIFT; } if (modifierName.equalsIgnoreCase(Action.findModifierString(SWT.ALT))) { return SWT.ALT; } if (modifierName .equalsIgnoreCase(Action.findModifierString(SWT.COMMAND))) { return SWT.COMMAND; } return 0; } /** * Computes the state mask for the given modifier string. * * @param modifiers * the string with the modifiers, separated by '+', '-', ';', ',' * or '.' * @return the state mask or {@code -1} if the input is invalid */ private static final int computeStateMask(String modifiers) { if (modifiers == null) { return -1; } if (modifiers.length() == 0) { return SWT.NONE; } int stateMask = 0; StringTokenizer modifierTokenizer = new StringTokenizer(modifiers, ",;.:+-* "); //$NON-NLS-1$ while (modifierTokenizer.hasMoreTokens()) { int modifier = findLocalizedModifier(modifierTokenizer.nextToken()); if (modifier == 0 || (stateMask & modifier) == modifier) { return -1; } stateMask = stateMask | modifier; } return stateMask; } }