/******************************************************************************* * Copyright (c) 2008, 2012 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 *******************************************************************************/ package org.eclipse.jface.text.hyperlink; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.ShellAdapter; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.util.Geometry; import org.eclipse.jface.util.Util; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.text.AbstractInformationControl; import org.eclipse.jface.text.AbstractInformationControlManager; import org.eclipse.jface.text.IInformationControl; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.IInformationControlExtension2; import org.eclipse.jface.text.IInformationControlExtension3; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextHover; import org.eclipse.jface.text.ITextHoverExtension; import org.eclipse.jface.text.ITextHoverExtension2; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.IWidgetTokenKeeper; import org.eclipse.jface.text.IWidgetTokenKeeperExtension; import org.eclipse.jface.text.IWidgetTokenOwner; import org.eclipse.jface.text.IWidgetTokenOwnerExtension; import org.eclipse.jface.text.JFaceTextUtil; import org.eclipse.jface.text.Region; /** * A hyperlink presenter capable of showing multiple hyperlinks in a hover. * * @since 3.4 */ public class MultipleHyperlinkPresenter extends DefaultHyperlinkPresenter implements IHyperlinkPresenterExtension2 { private static final boolean IS_OLD_WINDOWS; static { int majorVersion= Integer.MAX_VALUE; if (Util.isWin32()) { String osVersion= System.getProperty("os.version"); //$NON-NLS-1$ if (osVersion != null) { int majorIndex = osVersion.indexOf('.'); if (majorIndex != -1) { osVersion = osVersion.substring(0, majorIndex); try { majorVersion= Integer.parseInt(osVersion); } catch (NumberFormatException exception) { // use default } } } } IS_OLD_WINDOWS= majorVersion < 6; // before Vista (6.0) } private static final boolean IS_MAC= Util.isMac(); private static final boolean IS_GTK= Util.isGtk(); /** * An information control capable of showing a list of hyperlinks. The hyperlinks can be opened. */ private static class LinkListInformationControl extends AbstractInformationControl implements IInformationControlExtension2 { private static final class LinkContentProvider implements IStructuredContentProvider { @Override public Object[] getElements(Object inputElement) { return (Object[]) inputElement; } @Override public void dispose() { } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } } private static final class LinkLabelProvider extends ColumnLabelProvider { @Override public String getText(Object element) { IHyperlink link= (IHyperlink)element; String text= link.getHyperlinkText(); if (text != null) return text; return HyperlinkMessages.getString("LinkListInformationControl.unknownLink"); //$NON-NLS-1$ } } private final MultipleHyperlinkHoverManager fManager; private IHyperlink[] fInput; private Composite fParent; private Table fTable; private final Color fForegroundColor; private final Color fBackgroundColor; /** * Creates a link list information control with the given shell as parent. * * @param parentShell the parent shell * @param manager the hover manager * @param foregroundColor the foreground color, must not be disposed * @param backgroundColor the background color, must not be disposed */ public LinkListInformationControl(Shell parentShell, MultipleHyperlinkHoverManager manager, Color foregroundColor, Color backgroundColor) { super(parentShell, false); fManager= manager; fForegroundColor= foregroundColor; fBackgroundColor= backgroundColor; create(); } @Override public void setInformation(String information) { //replaced by IInformationControlExtension2#setInput(java.lang.Object) } @Override public void setInput(Object input) { fInput= (IHyperlink[]) input; deferredCreateContent(fParent); } @Override protected void createContent(Composite parent) { fParent= parent; GridLayout layout= new GridLayout(); if (IS_OLD_WINDOWS) { layout.marginWidth= 0; layout.marginHeight= 4; layout.marginRight= 4; } else if (IS_MAC) { layout.marginWidth= 4; layout.marginHeight= 0; layout.marginTop= 4; layout.marginBottom= 4 - 1; } else if (IS_GTK) { layout.marginWidth= 4; layout.marginHeight= 0; layout.marginTop= 4; layout.marginBottom= 4 - 2; } else { layout.marginWidth= 4; layout.marginHeight= 4; } fParent.setLayout(layout); fParent.setForeground(fForegroundColor); fParent.setBackground(fBackgroundColor); } @Override public Point computeSizeHint() { Point preferedSize= getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT, true); Point constraints= getSizeConstraints(); if (constraints == null) return preferedSize; if (fTable.getVerticalBar() == null || fTable.getHorizontalBar() == null) return Geometry.min(constraints, preferedSize); int scrollBarWidth= fTable.getVerticalBar().getSize().x; int scrollBarHeight= fTable.getHorizontalBar().getSize().y; if (IS_MAC && fTable.getScrollbarsMode() == SWT.SCROLLBAR_OVERLAY) { // workaround for https://bugs.eclipse.org/387732 : [10.8] Table scrollbar width is 16 (not 15) on Mountain Lion scrollBarWidth--; scrollBarHeight--; } int width; if (preferedSize.y - scrollBarHeight <= constraints.y) { width= preferedSize.x - scrollBarWidth; fTable.getVerticalBar().setVisible(false); } else { width= Math.min(preferedSize.x, constraints.x); } int height; if (preferedSize.x - scrollBarWidth <= constraints.x) { height= preferedSize.y - scrollBarHeight; fTable.getHorizontalBar().setVisible(false); } else { height= Math.min(preferedSize.y, constraints.y); } return new Point(width, height); } private void deferredCreateContent(Composite parent) { fTable= new Table(parent, SWT.SINGLE | SWT.FULL_SELECTION); fTable.setLinesVisible(false); fTable.setHeaderVisible(false); fTable.setForeground(fForegroundColor); fTable.setBackground(fBackgroundColor); fTable.setFont(JFaceResources.getDialogFont()); GridData data= new GridData(SWT.BEGINNING, SWT.BEGINNING, true, true); fTable.setLayoutData(data); final TableViewer viewer= new TableViewer(fTable); viewer.setContentProvider(new LinkContentProvider()); viewer.setLabelProvider(new LinkLabelProvider()); viewer.setInput(fInput); fTable.setSelection(0); registerTableListeners(); getShell().addShellListener(new ShellAdapter() { @Override public void shellActivated(ShellEvent e) { if (viewer.getTable().getSelectionCount() == 0) { viewer.getTable().setSelection(0); } viewer.getTable().setFocus(); } }); } private void registerTableListeners() { fTable.addMouseMoveListener(new MouseMoveListener() { TableItem fLastItem= null; @Override public void mouseMove(MouseEvent e) { if (fTable.equals(e.getSource())) { Object o= fTable.getItem(new Point(e.x, e.y)); if (fLastItem == null ^ o == null) { fTable.setCursor(o == null ? null : fTable.getDisplay().getSystemCursor(SWT.CURSOR_HAND)); } if (o instanceof TableItem) { TableItem item= (TableItem) o; if (!o.equals(fLastItem)) { fLastItem= (TableItem) o; fTable.setSelection(new TableItem[] { fLastItem }); } else if (e.y < fTable.getItemHeight() / 4) { // Scroll up int index= fTable.indexOf(item); if (index > 0) { fLastItem= fTable.getItem(index - 1); fTable.setSelection(new TableItem[] { fLastItem }); } } else if (e.y > fTable.getBounds().height - fTable.getItemHeight() / 4) { // Scroll down int index= fTable.indexOf(item); if (index < fTable.getItemCount() - 1) { fLastItem= fTable.getItem(index + 1); fTable.setSelection(new TableItem[] { fLastItem }); } } } else if (o == null) { fLastItem= null; } } } }); fTable.addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(SelectionEvent e) { openSelectedLink(); } }); fTable.addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent e) { if (fTable.getSelectionCount() < 1) return; if (e.button != 1) return; if (fTable.equals(e.getSource())) { Object o= fTable.getItem(new Point(e.x, e.y)); TableItem selection= fTable.getSelection()[0]; if (selection.equals(o)) openSelectedLink(); } } }); fTable.addTraverseListener(new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { if (e.keyCode == SWT.ESC) { fManager.hideInformationControl(); } } }); } @Override public boolean hasContents() { return true; } /** * Opens the currently selected link. */ private void openSelectedLink() { if (fTable.getSelectionCount() < 1) return; TableItem selection= fTable.getSelection()[0]; IHyperlink link= (IHyperlink)selection.getData(); fManager.hideInformationControl(); fManager.setCaret(); link.open(); } } private class MultipleHyperlinkHover implements ITextHover, ITextHoverExtension, ITextHoverExtension2 { /** * @see org.eclipse.jface.text.ITextHover#getHoverInfo(org.eclipse.jface.text.ITextViewer, org.eclipse.jface.text.IRegion) * @deprecated As of 3.4, replaced by * {@link ITextHoverExtension2#getHoverInfo2(ITextViewer, IRegion)} */ @Deprecated @Override public String getHoverInfo(ITextViewer textViewer, IRegion hoverRegion) { return null; } @Override public IRegion getHoverRegion(ITextViewer textViewer, int offset) { return fSubjectRegion; } @Override public Object getHoverInfo2(ITextViewer textViewer, IRegion hoverRegion) { return fHyperlinks; } @Override public IInformationControlCreator getHoverControlCreator() { return new IInformationControlCreator() { @Override public IInformationControl createInformationControl(Shell parent) { Color foregroundColor= fTextViewer.getTextWidget().getForeground(); Color backgroundColor= fTextViewer.getTextWidget().getBackground(); return new LinkListInformationControl(parent, fManager, foregroundColor, backgroundColor); } }; } } private static class MultipleHyperlinkHoverManager extends AbstractInformationControlManager implements IWidgetTokenKeeper, IWidgetTokenKeeperExtension { private class Closer implements IInformationControlCloser, Listener, KeyListener, MouseListener { private Control fSubjectControl; private Display fDisplay; private IInformationControl fControl; private Rectangle fSubjectArea; @Override public void setInformationControl(IInformationControl control) { fControl= control; } @Override public void setSubjectControl(Control subject) { fSubjectControl= subject; } @Override public void start(Rectangle subjectArea) { fSubjectArea= subjectArea; fDisplay= fSubjectControl.getDisplay(); if (!fDisplay.isDisposed()) { fDisplay.addFilter(SWT.FocusOut, this); fDisplay.addFilter(SWT.MouseMove, this); fTextViewer.getTextWidget().addKeyListener(this); fTextViewer.getTextWidget().addMouseListener(this); } } @Override public void stop() { if (fDisplay != null && !fDisplay.isDisposed()) { fDisplay.removeFilter(SWT.FocusOut, this); fDisplay.removeFilter(SWT.MouseMove, this); fTextViewer.getTextWidget().removeKeyListener(this); fTextViewer.getTextWidget().removeMouseListener(this); } fSubjectArea= null; } @Override public void handleEvent(Event event) { switch (event.type) { case SWT.FocusOut: if (!fControl.isFocusControl()) disposeInformationControl(); break; case SWT.MouseMove: handleMouseMove(event); break; } } /** * Handle mouse movement events. * * @param event the event */ private void handleMouseMove(Event event) { if (!(event.widget instanceof Control)) return; if (fControl.isFocusControl()) return; Control eventControl= (Control) event.widget; //transform coordinates to subject control: Point mouseLoc= event.display.map(eventControl, fSubjectControl, event.x, event.y); if (fSubjectArea.contains(mouseLoc)) return; if (inKeepUpZone(mouseLoc.x, mouseLoc.y, ((IInformationControlExtension3) fControl).getBounds())) return; if (!isTakingFocusWhenVisible()) hideInformationControl(); } /** * Tests whether a given mouse location is within the keep-up zone. * The hover should not be hidden as long as the mouse stays inside this zone. * * @param x the x coordinate, relative to the <em>subject control</em> * @param y the y coordinate, relative to the <em>subject control</em> * @param controlBounds the bounds of the current control * * @return <code>true</code> iff the mouse event occurred in the keep-up zone */ private boolean inKeepUpZone(int x, int y, Rectangle controlBounds) { // +-----------+ // |subjectArea| // +-----------+ // |also keepUp| // ++-----------+-------+ // | totalBounds | // +--------------------+ if (fSubjectArea.contains(x, y)) return true; Rectangle iControlBounds= fSubjectControl.getDisplay().map(null, fSubjectControl, controlBounds); Rectangle totalBounds= Geometry.copy(iControlBounds); if (totalBounds.contains(x, y)) return true; int keepUpY= fSubjectArea.y + fSubjectArea.height; Rectangle alsoKeepUp= new Rectangle(fSubjectArea.x, keepUpY, fSubjectArea.width, totalBounds.y - keepUpY); return alsoKeepUp.contains(x, y); } @Override public void keyPressed(KeyEvent e) { hideInformationControl(); } @Override public void keyReleased(KeyEvent e) { if (!isTakingFocusWhenVisible()) hideInformationControl(); } @Override public void mouseDoubleClick(MouseEvent e) { } @Override public void mouseDown(MouseEvent e) { } @Override public void mouseUp(MouseEvent e) { hideInformationControl(); } } /** * Priority of the hover managed by this manager. * Default value: One higher then for the hovers * managed by TextViewerHoverManager. */ private static final int WIDGET_TOKEN_PRIORITY= 1; private final MultipleHyperlinkHover fHover; private final ITextViewer fTextViewer; private final MultipleHyperlinkPresenter fHyperlinkPresenter; private final Closer fCloser; private boolean fIsControlVisible; /** * Create a new MultipleHyperlinkHoverManager. The MHHM can show and hide * the given MultipleHyperlinkHover inside the given ITextViewer. * * @param hover the hover to manage * @param viewer the viewer to show the hover in * @param hyperlinkPresenter the hyperlink presenter using this manager to present hyperlinks */ public MultipleHyperlinkHoverManager(MultipleHyperlinkHover hover, ITextViewer viewer, MultipleHyperlinkPresenter hyperlinkPresenter) { super(hover.getHoverControlCreator()); fHover= hover; fTextViewer= viewer; fHyperlinkPresenter= hyperlinkPresenter; fCloser= new Closer(); setCloser(fCloser); fIsControlVisible= false; } @Override protected void computeInformation() { IRegion region= fHover.getHoverRegion(fTextViewer, -1); if (region == null) { setInformation(null, null); return; } Rectangle area= JFaceTextUtil.computeArea(region, fTextViewer); if (area == null || area.isEmpty()) { setInformation(null, null); return; } Object information= fHover.getHoverInfo2(fTextViewer, region); setCustomInformationControlCreator(fHover.getHoverControlCreator()); setInformation(information, area); } @Override protected Point computeInformationControlLocation(Rectangle subjectArea, Point controlSize) { Point result= super.computeInformationControlLocation(subjectArea, controlSize); Point cursorLocation= fTextViewer.getTextWidget().getDisplay().getCursorLocation(); if (isTakingFocusWhenVisible() || cursorLocation.x <= result.x + controlSize.x) return result; result.x= cursorLocation.x + 20 - controlSize.x; return result; } @Override protected void showInformationControl(Rectangle subjectArea) { if (fTextViewer instanceof IWidgetTokenOwnerExtension) { if (((IWidgetTokenOwnerExtension)fTextViewer).requestWidgetToken(this, WIDGET_TOKEN_PRIORITY)) { super.showInformationControl(subjectArea); fIsControlVisible= true; } } else if (fTextViewer instanceof IWidgetTokenOwner) { if (((IWidgetTokenOwner)fTextViewer).requestWidgetToken(this)) { super.showInformationControl(subjectArea); fIsControlVisible= true; } } else { super.showInformationControl(subjectArea); fIsControlVisible= true; } } /** * Sets the caret where hyperlinking got initiated. * * @since 3.5 */ private void setCaret() { fHyperlinkPresenter.setCaret(); } @Override protected void hideInformationControl() { super.hideInformationControl(); if (fTextViewer instanceof IWidgetTokenOwner) { ((IWidgetTokenOwner) fTextViewer).releaseWidgetToken(this); } fIsControlVisible= false; fHyperlinkPresenter.hideHyperlinks(); } @Override public void disposeInformationControl() { super.disposeInformationControl(); if (fTextViewer instanceof IWidgetTokenOwner) { ((IWidgetTokenOwner) fTextViewer).releaseWidgetToken(this); } fIsControlVisible= false; fHyperlinkPresenter.hideHyperlinks(); } @Override public boolean requestWidgetToken(IWidgetTokenOwner owner) { hideInformationControl(); return true; } @Override public boolean requestWidgetToken(IWidgetTokenOwner owner, int priority) { if (priority < WIDGET_TOKEN_PRIORITY) return false; hideInformationControl(); return true; } @Override public boolean setFocus(IWidgetTokenOwner owner) { return isTakingFocusWhenVisible(); } /** * Returns <code>true</code> if the information control managed by * this manager is visible, <code>false</code> otherwise. * * @return <code>true</code> if information control is visible */ public boolean isInformationControlVisible() { return fIsControlVisible; } } private ITextViewer fTextViewer; private IHyperlink[] fHyperlinks; private Region fSubjectRegion; private MultipleHyperlinkHoverManager fManager; /** * The offset in the text viewer where hyperlinking got initiated. * @since 3.5 */ private int fCursorOffset; /** * Creates a new multiple hyperlink presenter which uses {@link #HYPERLINK_COLOR} to read the * color from the given preference store. * * @param store the preference store */ public MultipleHyperlinkPresenter(IPreferenceStore store) { super(store); } /** * Creates a new multiple hyperlink presenter. * * @param color the hyperlink color, to be disposed by the caller */ public MultipleHyperlinkPresenter(RGB color) { super(color); } @Override public void install(ITextViewer viewer) { super.install(viewer); fTextViewer= viewer; fManager= new MultipleHyperlinkHoverManager(new MultipleHyperlinkHover(), fTextViewer, this); fManager.install(viewer.getTextWidget()); fManager.setSizeConstraints(100, 12, false, true); } @Override public void uninstall() { super.uninstall(); if (fTextViewer != null) { fManager.dispose(); fTextViewer= null; } } @Override public boolean canShowMultipleHyperlinks() { return true; } @Override public boolean canHideHyperlinks() { return !fManager.isInformationControlVisible(); } @Override public void hideHyperlinks() { super.hideHyperlinks(); fHyperlinks= null; } @Override public void showHyperlinks(IHyperlink[] hyperlinks) { showHyperlinks(hyperlinks, false); } /** * {@inheritDoc} * * @since 3.7 */ @Override public void showHyperlinks(IHyperlink[] activeHyperlinks, boolean takesFocusWhenVisible) { fManager.takesFocusWhenVisible(takesFocusWhenVisible); super.showHyperlinks(new IHyperlink[] { activeHyperlinks[0] }); fSubjectRegion= null; fHyperlinks= activeHyperlinks; if (activeHyperlinks.length == 1) return; int start= activeHyperlinks[0].getHyperlinkRegion().getOffset(); int end= start + activeHyperlinks[0].getHyperlinkRegion().getLength(); for (int i= 1; i < activeHyperlinks.length; i++) { int hstart= activeHyperlinks[i].getHyperlinkRegion().getOffset(); int hend= hstart + activeHyperlinks[i].getHyperlinkRegion().getLength(); start= Math.min(start, hstart); end= Math.max(end, hend); } fSubjectRegion= new Region(start, end - start); fCursorOffset= JFaceTextUtil.getOffsetForCursorLocation(fTextViewer); fManager.showInformation(); } /** * Sets the caret where hyperlinking got initiated. * * @since 3.5 */ private void setCaret() { Point selectedRange= fTextViewer.getSelectedRange(); if (fCursorOffset != -1 && !(fSubjectRegion.getOffset() <= selectedRange.x && selectedRange.x + selectedRange.y <= fSubjectRegion.getOffset() + fSubjectRegion.getLength())) fTextViewer.setSelectedRange(fCursorOffset, 0); } }