| /******************************************************************************* |
| * Copyright (c) 2000, 2015 IBM Corporation and others. |
| * 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 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Steffen Pingel <steffen.pingel@tasktop.com> (Tasktop Technologies Inc.) - [navigation] hyperlink decoration is not erased when mouse is moved out of Text widget - https://bugs.eclipse.org/bugs/show_bug.cgi?id=100278 |
| *******************************************************************************/ |
| package org.eclipse.jface.text.hyperlink; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.events.FocusEvent; |
| import org.eclipse.swt.events.FocusListener; |
| import org.eclipse.swt.events.KeyEvent; |
| import org.eclipse.swt.events.KeyListener; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseListener; |
| import org.eclipse.swt.events.MouseMoveListener; |
| import org.eclipse.swt.events.MouseTrackListener; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Listener; |
| |
| import org.eclipse.core.runtime.Assert; |
| |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITextListener; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.JFaceTextUtil; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.jface.text.TextEvent; |
| import org.eclipse.jface.text.TextViewer; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| |
| |
| /** |
| * Default implementation of a hyperlink manager. |
| * |
| * @since 3.1 |
| */ |
| public class HyperlinkManager implements ITextListener, Listener, KeyListener, MouseListener, MouseMoveListener, FocusListener, MouseTrackListener { |
| |
| |
| /** |
| * Text operation code for requesting to open the hyperlink at the caret position. |
| * @see #openHyperlink() |
| * @since 3.6 |
| */ |
| public static final int OPEN_HYPERLINK= ISourceViewer.QUICK_ASSIST + 1; |
| |
| |
| /** |
| * Detection strategy. |
| */ |
| public static final class DETECTION_STRATEGY { |
| |
| String fName; |
| |
| private DETECTION_STRATEGY(String name) { |
| fName= name; |
| } |
| |
| @Override |
| public String toString() { |
| return fName; |
| } |
| } |
| |
| |
| /** |
| * The first detected hyperlink is passed to the |
| * hyperlink presenter and no further detector |
| * is consulted. |
| */ |
| public static final DETECTION_STRATEGY FIRST= new DETECTION_STRATEGY("first"); //$NON-NLS-1$ |
| |
| /** |
| * All detected hyperlinks from all detectors are collected |
| * and passed to the hyperlink presenter. |
| * <p> |
| * This strategy is only allowed if {@link IHyperlinkPresenter#canShowMultipleHyperlinks()} |
| * returns <code>true</code>. |
| * </p> |
| */ |
| public static final DETECTION_STRATEGY ALL= new DETECTION_STRATEGY("all"); //$NON-NLS-1$ |
| |
| /** |
| * All detected hyperlinks from all detectors are collected |
| * and all those with the longest region are passed to the |
| * hyperlink presenter. |
| * <p> |
| * This strategy is only allowed if {@link IHyperlinkPresenter#canShowMultipleHyperlinks()} |
| * returns <code>true</code>. |
| * </p> |
| */ |
| public static final DETECTION_STRATEGY LONGEST_REGION_ALL= new DETECTION_STRATEGY("all with same longest region"); //$NON-NLS-1$ |
| |
| /** |
| * All detected hyperlinks from all detectors are collected |
| * and form all those with the longest region only the first |
| * one is passed to the hyperlink presenter. |
| */ |
| public static final DETECTION_STRATEGY LONGEST_REGION_FIRST= new DETECTION_STRATEGY("first with longest region"); //$NON-NLS-1$ |
| |
| |
| /** The text viewer on which this hyperlink manager works. */ |
| private ITextViewer fTextViewer; |
| /** The session is active. */ |
| private boolean fActive; |
| /** The key modifier mask of the default hyperlink modifier. */ |
| private int fHyperlinkStateMask; |
| /** |
| * The active key modifier mask. |
| * @since 3.3 |
| */ |
| private int fActiveHyperlinkStateMask; |
| /** The active hyperlinks. */ |
| private IHyperlink[] fActiveHyperlinks; |
| /** The hyperlink detectors. */ |
| private IHyperlinkDetector[] fHyperlinkDetectors; |
| /** The hyperlink presenter. */ |
| private IHyperlinkPresenter fHyperlinkPresenter; |
| /** The detection strategy. */ |
| private final DETECTION_STRATEGY fDetectionStrategy; |
| |
| |
| /** |
| * Creates a new hyperlink manager. |
| * |
| * @param detectionStrategy the detection strategy one of {{@link #ALL}, {@link #FIRST}, {@link #LONGEST_REGION_ALL}, {@link #LONGEST_REGION_FIRST}} |
| */ |
| public HyperlinkManager(DETECTION_STRATEGY detectionStrategy) { |
| Assert.isNotNull(detectionStrategy); |
| fDetectionStrategy= detectionStrategy; |
| } |
| |
| /** |
| * Installs this hyperlink manager with the given arguments. |
| * |
| * @param textViewer the text viewer |
| * @param hyperlinkPresenter the hyperlink presenter |
| * @param hyperlinkDetectors the array of hyperlink detectors, must not be empty |
| * @param eventStateMask the SWT event state mask to activate hyperlink mode |
| */ |
| public void install(ITextViewer textViewer, IHyperlinkPresenter hyperlinkPresenter, IHyperlinkDetector[] hyperlinkDetectors, int eventStateMask) { |
| Assert.isNotNull(textViewer); |
| Assert.isNotNull(hyperlinkPresenter); |
| fTextViewer= textViewer; |
| fHyperlinkPresenter= hyperlinkPresenter; |
| Assert.isLegal(fHyperlinkPresenter.canShowMultipleHyperlinks() || fDetectionStrategy == FIRST || fDetectionStrategy == LONGEST_REGION_FIRST); |
| setHyperlinkDetectors(hyperlinkDetectors); |
| setHyperlinkStateMask(eventStateMask); |
| |
| StyledText text= fTextViewer.getTextWidget(); |
| if (text == null || text.isDisposed()) |
| return; |
| |
| text.getDisplay().addFilter(SWT.KeyUp, this); |
| text.addKeyListener(this); |
| text.addMouseListener(this); |
| text.addMouseMoveListener(this); |
| text.addFocusListener(this); |
| text.addMouseTrackListener(this); |
| |
| fTextViewer.addTextListener(this); |
| |
| fHyperlinkPresenter.install(fTextViewer); |
| } |
| |
| /** |
| * Sets the hyperlink detectors for this hyperlink manager. |
| * <p> |
| * It is allowed to call this method after this |
| * hyperlink manger has been installed. |
| * </p> |
| * |
| * @param hyperlinkDetectors and array of hyperlink detectors, must not be empty |
| */ |
| public void setHyperlinkDetectors(IHyperlinkDetector[] hyperlinkDetectors) { |
| Assert.isTrue(hyperlinkDetectors != null && hyperlinkDetectors.length > 0); |
| if (fHyperlinkDetectors == null) |
| fHyperlinkDetectors= hyperlinkDetectors; |
| else { |
| synchronized (fHyperlinkDetectors) { |
| fHyperlinkDetectors= hyperlinkDetectors; |
| } |
| } |
| } |
| |
| /** |
| * Sets the SWT event state mask which in combination |
| * with the left mouse button triggers the hyperlink mode. |
| * <p> |
| * It is allowed to call this method after this |
| * hyperlink manger has been installed. |
| * </p> |
| * <p> |
| * Note that {@link IHyperlinkDetectorExtension2}s may specify additional state masks. |
| * </p> |
| * |
| * @param eventStateMask the SWT event state mask to activate hyperlink mode |
| */ |
| public void setHyperlinkStateMask(int eventStateMask) { |
| fHyperlinkStateMask= eventStateMask; |
| } |
| |
| /** |
| * Uninstalls this hyperlink manager. |
| */ |
| public void uninstall() { |
| deactivate(); |
| |
| StyledText text= fTextViewer.getTextWidget(); |
| if (text != null && !text.isDisposed()) { |
| text.removeKeyListener(this); |
| text.getDisplay().removeFilter(SWT.KeyUp, this); |
| text.removeMouseListener(this); |
| text.removeMouseMoveListener(this); |
| text.removeFocusListener(this); |
| text.removeMouseTrackListener(this); |
| } |
| fTextViewer.removeTextListener(this); |
| |
| fHyperlinkPresenter.uninstall(); |
| |
| fHyperlinkPresenter= null; |
| fTextViewer= null; |
| fHyperlinkDetectors= null; |
| } |
| |
| /** |
| * Deactivates the currently shown hyperlinks. |
| */ |
| protected void deactivate() { |
| fHyperlinkPresenter.hideHyperlinks(); |
| fActive= false; |
| } |
| |
| /** |
| * Finds hyperlinks at the current offset. |
| * |
| * @return the hyperlinks or <code>null</code> if none. |
| */ |
| protected IHyperlink[] findHyperlinks() { |
| int offset= getCurrentTextOffset(); |
| if (offset == -1) |
| return null; |
| |
| IRegion region= new Region(offset, 0); |
| return findHyperlinks(region); |
| } |
| |
| /** |
| * Returns the hyperlinks in the given region or <code>null</code> if none. |
| * |
| * @param region the selection region |
| * @return the array of hyperlinks found or <code>null</code> if none |
| * @since 3.7 |
| */ |
| private IHyperlink[] findHyperlinks(IRegion region) { |
| List<IHyperlink> allHyperlinks= new ArrayList<>(fHyperlinkDetectors.length * 2); |
| synchronized (fHyperlinkDetectors) { |
| for (int i= 0, length= fHyperlinkDetectors.length; i < length; i++) { |
| IHyperlinkDetector detector= fHyperlinkDetectors[i]; |
| if (detector == null) |
| continue; |
| |
| if (detector instanceof IHyperlinkDetectorExtension2) { |
| int stateMask= ((IHyperlinkDetectorExtension2)detector).getStateMask(); |
| if (stateMask != -1 && stateMask != fActiveHyperlinkStateMask) |
| continue; |
| else if (stateMask == -1 && fActiveHyperlinkStateMask != fHyperlinkStateMask) |
| continue; |
| } else if (fActiveHyperlinkStateMask != fHyperlinkStateMask) |
| continue; |
| |
| boolean canShowMultipleHyperlinks= fHyperlinkPresenter.canShowMultipleHyperlinks(); |
| IHyperlink[] hyperlinks= detector.detectHyperlinks(fTextViewer, region, canShowMultipleHyperlinks); |
| if (hyperlinks == null) |
| continue; |
| |
| Assert.isLegal(hyperlinks.length > 0); |
| |
| if (fDetectionStrategy == FIRST) { |
| if (hyperlinks.length == 1) |
| return hyperlinks; |
| return new IHyperlink[] {hyperlinks[0]}; |
| } |
| allHyperlinks.addAll(Arrays.asList(hyperlinks)); |
| } |
| } |
| |
| if (allHyperlinks.isEmpty()) |
| return null; |
| |
| if (fDetectionStrategy != ALL) { |
| int maxLength= computeLongestHyperlinkLength(allHyperlinks); |
| Iterator<IHyperlink> iter= new ArrayList<>(allHyperlinks).iterator(); |
| while (iter.hasNext()) { |
| IHyperlink hyperlink= iter.next(); |
| if (hyperlink.getHyperlinkRegion().getLength() < maxLength) |
| allHyperlinks.remove(hyperlink); |
| } |
| } |
| |
| if (fDetectionStrategy == LONGEST_REGION_FIRST) |
| return new IHyperlink[] {allHyperlinks.get(0)}; |
| |
| return allHyperlinks.toArray(new IHyperlink[allHyperlinks.size()]); |
| |
| } |
| |
| /** |
| * Computes the length of the longest detected hyperlink. |
| * |
| * @param hyperlinks the list of hyperlinks |
| * @return the length of the longest detected |
| */ |
| protected int computeLongestHyperlinkLength(List<? extends IHyperlink> hyperlinks) { |
| Assert.isLegal(hyperlinks != null && !hyperlinks.isEmpty()); |
| Iterator<? extends IHyperlink> iter= hyperlinks.iterator(); |
| int length= Integer.MIN_VALUE; |
| while (iter.hasNext()) { |
| IRegion region= iter.next().getHyperlinkRegion(); |
| if (region.getLength() < length) |
| continue; |
| length= region.getLength(); |
| } |
| return length; |
| } |
| |
| /** |
| * Returns the offset in the given viewer that corresponds to the current cursor location. |
| * |
| * @return the offset in the given viewer that corresponds to the current cursor location. |
| */ |
| protected int getCurrentTextOffset() { |
| return JFaceTextUtil.getOffsetForCursorLocation(fTextViewer); |
| } |
| |
| @Override |
| public void keyPressed(KeyEvent event) { |
| |
| if (!isRegisteredStateMask((event.keyCode | event.stateMask) & SWT.MODIFIER_MASK)) { |
| deactivate(); |
| return; |
| } |
| |
| fActive= true; |
| |
| // do not show hyperlink, since that would often be confusing (e.g. when pressing Ctrl+C) |
| } |
| |
| @Override |
| public void keyReleased(KeyEvent event) { |
| } |
| |
| @Override |
| public void mouseDoubleClick(MouseEvent e) { |
| |
| } |
| |
| @Override |
| public void mouseDown(MouseEvent event) { |
| |
| if (fHyperlinkPresenter instanceof IHyperlinkPresenterExtension) { |
| if (!((IHyperlinkPresenterExtension)fHyperlinkPresenter).canHideHyperlinks()) |
| return; |
| } |
| |
| if (!isRegisteredStateMask(event.stateMask)) { |
| if (fActive) |
| deactivate(); |
| |
| return; |
| } |
| |
| if (event.button != 1) { |
| deactivate(); |
| return; |
| } |
| |
| fActive= true; |
| fActiveHyperlinkStateMask= event.stateMask & SWT.MODIFIER_MASK; |
| |
| StyledText text= fTextViewer.getTextWidget(); |
| if (text == null || text.isDisposed()) { |
| deactivate(); |
| return; |
| } |
| |
| if (text.isTextSelected()) { |
| deactivate(); |
| return; |
| } |
| |
| fActiveHyperlinks= findHyperlinks(); |
| showHyperlinks(false); |
| } |
| |
| @Override |
| public void mouseUp(MouseEvent e) { |
| |
| if (!fActive) { |
| fActiveHyperlinks= null; |
| return; |
| } |
| |
| if (e.button != 1) |
| fActiveHyperlinks= null; |
| |
| deactivate(); |
| |
| if (fActiveHyperlinks != null) |
| fActiveHyperlinks[0].open(); |
| } |
| |
| @Override |
| public void mouseMove(MouseEvent event) { |
| if (fHyperlinkPresenter instanceof IHyperlinkPresenterExtension) { |
| if (!((IHyperlinkPresenterExtension)fHyperlinkPresenter).canHideHyperlinks()) |
| return; |
| } |
| |
| if (!isRegisteredStateMask(event.stateMask)) { |
| if (fActive) |
| deactivate(); |
| |
| return; |
| } |
| |
| fActive= true; |
| fActiveHyperlinkStateMask= event.stateMask; |
| |
| StyledText text= fTextViewer.getTextWidget(); |
| if (text == null || text.isDisposed()) { |
| deactivate(); |
| return; |
| } |
| |
| if ((event.stateMask & SWT.BUTTON1) != 0 && text.isTextSelected()) { |
| deactivate(); |
| return; |
| } |
| |
| fActiveHyperlinks= findHyperlinks(); |
| showHyperlinks(false); |
| } |
| |
| /** |
| * Checks whether the given state mask is registered. |
| * |
| * @param stateMask the state mask |
| * @return <code>true</code> if a detector is registered for the given state mask |
| * @since 3.3 |
| */ |
| private boolean isRegisteredStateMask(int stateMask) { |
| if (stateMask == fHyperlinkStateMask) |
| return true; |
| |
| synchronized (fHyperlinkDetectors) { |
| for (int i= 0; i < fHyperlinkDetectors.length; i++) { |
| if (fHyperlinkDetectors[i] instanceof IHyperlinkDetectorExtension2) { |
| if (stateMask == ((IHyperlinkDetectorExtension2)fHyperlinkDetectors[i]).getStateMask()) |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void focusGained(FocusEvent e) {} |
| |
| @Override |
| public void focusLost(FocusEvent event) { |
| deactivate(); |
| } |
| |
| @Override |
| public void handleEvent(Event event) { |
| //key up |
| deactivate(); |
| } |
| |
| @Override |
| public void textChanged(TextEvent event) { |
| if (event.getDocumentEvent() != null) |
| deactivate(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @since 3.4 |
| */ |
| @Override |
| public void mouseExit(MouseEvent e) { |
| if (fHyperlinkPresenter instanceof IHyperlinkPresenterExtension) { |
| if (!((IHyperlinkPresenterExtension)fHyperlinkPresenter).canHideHyperlinks()) |
| return; |
| } |
| deactivate(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @since 3.4 |
| */ |
| @Override |
| public void mouseEnter(MouseEvent e) { |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @since 3.4 |
| */ |
| @Override |
| public void mouseHover(MouseEvent e) { |
| } |
| |
| /** |
| * Opens the hyperlink at the current caret location directly if there's only one link, else |
| * opens the hyperlink control showing all the hyperlinks at that location. |
| * |
| * @param takesFocusWhenVisible <code>true</code> if the control takes focus when visible, |
| * <code>false</code> otherwise |
| * |
| * @return <code>true</code> if at least one hyperlink has been found at the caret location, |
| * <code>false</code> otherwise |
| * @since 3.7 |
| */ |
| private boolean showHyperlinks(boolean takesFocusWhenVisible) { |
| |
| if (fActiveHyperlinks == null || fActiveHyperlinks.length == 0) { |
| fHyperlinkPresenter.hideHyperlinks(); |
| return false; |
| } |
| if (fActiveHyperlinks.length == 1 && takesFocusWhenVisible) { |
| fActiveHyperlinks[0].open(); |
| } else { |
| if (fHyperlinkPresenter instanceof IHyperlinkPresenterExtension2) |
| ((IHyperlinkPresenterExtension2)fHyperlinkPresenter).showHyperlinks(fActiveHyperlinks, takesFocusWhenVisible); |
| else |
| fHyperlinkPresenter.showHyperlinks(fActiveHyperlinks); |
| } |
| return true; |
| |
| } |
| |
| /** |
| * Opens the hyperlink at the caret location or opens a chooser |
| * if more than one hyperlink is available. |
| * |
| * @return <code>true</code> if at least one hyperlink has been found at the caret location, <code>false</code> otherwise |
| * @see #OPEN_HYPERLINK |
| * @since 3.6 |
| */ |
| public boolean openHyperlink() { |
| fActiveHyperlinkStateMask= fHyperlinkStateMask; |
| |
| if (fHyperlinkPresenter instanceof IHyperlinkPresenterExtension) { |
| if (!((IHyperlinkPresenterExtension)fHyperlinkPresenter).canHideHyperlinks()) |
| return false; |
| } |
| ITextSelection sel= (ITextSelection)((TextViewer)fTextViewer).getSelection(); |
| int offset= sel.getOffset(); |
| if (offset == -1) |
| return false; |
| |
| IRegion region= new Region(offset, 0); |
| fActiveHyperlinks= findHyperlinks(region); |
| return showHyperlinks(true); |
| } |
| } |