/** * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.core.tooltips.presenter; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.bindings.keys.KeySequence; import org.eclipse.jface.text.AbstractInformationControlManager; import org.eclipse.jface.text.DefaultInformationControl.IInformationPresenter; import org.eclipse.jface.text.IInformationControl; 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.util.Geometry; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; 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.graphics.Point; import org.eclipse.swt.graphics.Rectangle; 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.python.pydev.core.docutils.StringUtils; import org.python.pydev.core.tooltips.presenter.InformationPresenterHelpers.PyInformationControl; import com.aptana.shared_core.bindings.KeyBindingHelper; /** * Based on org.eclipse.jface.text.information.InformationPresenter (but without the references to * an org.eclipse.jface.text.information.InformationPresenter nor partitions). * * To be used on controls to show tooltips that 'stick' and the user can interact with. */ public final class InformationPresenterControlManager extends AbstractInformationControlManager implements IWidgetTokenKeeper, IWidgetTokenKeeperExtension, IInformationPresenterControlManager { /** * The string that should be shown below the tooltip (if not given, a default is provided). */ private final String tooltipAffordanceString; /** * @param tooltipAffordanceString if null, a default is given. */ public InformationPresenterControlManager(IInformationPresenter presenter, String tooltipAffordanceString) { super(new InformationPresenterHelpers.TooltipInformationControlCreator(presenter)); this.tooltipAffordanceString = tooltipAffordanceString; if (presenter instanceof IInformationPresenterAsTooltip) { IInformationPresenterAsTooltip presenterAsTooltip = (IInformationPresenterAsTooltip) presenter; presenterAsTooltip.setInformationPresenterControlManager(this); } setCloser(new Closer()); takesFocusWhenVisible(true); ((InformationPresenterHelpers.TooltipInformationControlCreator) this.fInformationControlCreator) .setInformationPresenterControlManager(this); } /** * Priority of the info controls managed by this information presenter. * Default value: <code>5</code>. */ public static final int WIDGET_PRIORITY = 5; /** * Internal information control closer. Listens to several events issued by its subject control * and closes the information control when necessary. */ class Closer implements IInformationControlCloser, ControlListener, MouseListener, FocusListener, KeyListener, MouseMoveListener, Listener { /** The subject control. */ private Control fSubjectControl; /** The information control. */ private PyInformationControl fInformationControlToClose; /** Indicates whether this closer is active. */ private boolean fIsActive = false; private Rectangle fShellTooltipArea; private Display fDisplay; /* * @see IInformationControlCloser#setSubjectControl(Control) */ public void setSubjectControl(Control control) { fSubjectControl = control; } /* * @see IInformationControlCloser#setInformationControl(IInformationControl) */ public void setInformationControl(IInformationControl control) { Assert.isTrue(control == null || control instanceof PyInformationControl); fInformationControlToClose = (PyInformationControl) control; } /* * @see IInformationControlCloser#start(Rectangle) */ public void start(Rectangle informationArea) { fShellTooltipArea = fInformationControlToClose.getShellTooltipBounds(); if (fIsActive) { return; } fIsActive = true; if (fSubjectControl != null && !fSubjectControl.isDisposed()) { fSubjectControl.addControlListener(this); fSubjectControl.addMouseListener(this); fSubjectControl.addMouseMoveListener(this); fSubjectControl.addFocusListener(this); fSubjectControl.addKeyListener(this); fDisplay = fSubjectControl.getDisplay(); if (!fDisplay.isDisposed()) { fDisplay.addFilter(SWT.Activate, this); fDisplay.addFilter(SWT.MouseVerticalWheel, this); fDisplay.addFilter(SWT.FocusOut, this); fDisplay.addFilter(SWT.MouseDown, this); fDisplay.addFilter(SWT.MouseUp, this); fDisplay.addFilter(SWT.MouseMove, this); fDisplay.addFilter(SWT.MouseEnter, this); fDisplay.addFilter(SWT.MouseExit, this); fDisplay.addFilter(SWT.KeyDown, this); } } if (fInformationControlToClose != null) { fInformationControlToClose.addFocusListener(this); } } /* * @see IInformationControlCloser#stop() */ public void stop() { if (!fIsActive) { return; } fIsActive = false; if (fInformationControlToClose != null) { fInformationControlToClose.removeFocusListener(this); } if (fSubjectControl != null && !fSubjectControl.isDisposed()) { fSubjectControl.removeControlListener(this); fSubjectControl.removeMouseMoveListener(this); fSubjectControl.removeMouseListener(this); fSubjectControl.removeFocusListener(this); fSubjectControl.removeKeyListener(this); } if (fDisplay != null && !fDisplay.isDisposed()) { fDisplay.removeFilter(SWT.Activate, this); fDisplay.removeFilter(SWT.MouseVerticalWheel, this); fDisplay.removeFilter(SWT.FocusOut, this); fDisplay.removeFilter(SWT.MouseDown, this); fDisplay.removeFilter(SWT.MouseUp, this); fDisplay.removeFilter(SWT.MouseMove, this); fDisplay.removeFilter(SWT.MouseEnter, this); fDisplay.removeFilter(SWT.MouseExit, this); fDisplay.removeFilter(SWT.KeyDown, this); } fDisplay = null; } /* * @see ControlListener#controlResized(ControlEvent) */ public void controlResized(ControlEvent e) { hideInformationControl(); } /* * @see ControlListener#controlMoved(ControlEvent) */ public void controlMoved(ControlEvent e) { hideInformationControl(); } /* * @see MouseListener#mouseDown(MouseEvent) */ public void mouseDown(MouseEvent e) { hideInformationControl(); } /* * @see MouseListener#mouseUp(MouseEvent) */ public void mouseUp(MouseEvent e) { } /* * @see MouseListener#mouseDoubleClick(MouseEvent) */ public void mouseDoubleClick(MouseEvent e) { hideInformationControl(); } /* * @see FocusListener#focusGained(FocusEvent) */ public void focusGained(FocusEvent e) { } /* * @see FocusListener#focusLost(FocusEvent) */ public void focusLost(FocusEvent e) { Display d = fSubjectControl.getDisplay(); d.asyncExec(new Runnable() { // Without the asyncExec, mouse clicks to the workbench window are swallowed. public void run() { if (fInformationControlToClose == null || !fInformationControlToClose.isFocusControl()) { hideInformationControl(); } } }); } /* * @see KeyListener#keyPressed(KeyEvent) */ public void keyPressed(KeyEvent e) { hideInformationControl(); } /* * @see KeyListener#keyReleased(KeyEvent) */ public void keyReleased(KeyEvent e) { } public void mouseMove(MouseEvent e) { if (fInformationControl != null && fInformationControl.isFocusControl()) { if (!inKeepUpZone(e.x, e.y, fSubjectControl, fSubjectControl.getDisplay())) { hideInformationControl(); } } } public void handleEvent(Event event) { switch (event.type) { case SWT.Activate: case SWT.MouseVerticalWheel: case SWT.MouseUp: case SWT.MouseDown: case SWT.FocusOut: IInformationControl iControl = fInformationControl; if (iControl != null && !iControl.isFocusControl()) { hideInformationControl(); } break; case SWT.MouseMove: case SWT.MouseEnter: case SWT.MouseExit: handleMouseMove(event); break; case SWT.KeyDown: if (event.keyCode == SWT.ESC) { hideInformationControl(); } else if (fActivateEditorBinding != null && KeyBindingHelper.matchesKeybinding(event.keyCode, event.stateMask, fActivateEditorBinding)) { hideInformationControl(true, true); } break; } } private void handleMouseMove(Event event) { if (!(event.widget instanceof Control)) { return; } Control control = (Control) event.widget; Display display = control.getDisplay(); if (!inKeepUpZone(event.x, event.y, control, display)) { hideInformationControl(); } } /** * Note that all parameters (x, y, shellTooltipArea) must be in display coordinates. * @param control * @param display */ private boolean inKeepUpZone(int x, int y, Control control, Display display) { if (display.isDisposed()) { return true; //received something from a dead display? Let's keep on showing it... (not sure if this actually happens) } Point point = display.map(control, null, x, y); int margin = 20; //the bounds are in display coordinates Rectangle bounds = Geometry.copy(fShellTooltipArea); //expand so that we have some tolerance to keep it open Geometry.expand(bounds, margin, margin, margin, margin); return bounds.contains(point.x, point.y); } } private Control fControl; private ITooltipInformationProvider fProvider; private KeySequence fActivateEditorBinding; private Shell fInitiallyActiveShell; private Control fFocusControl; private boolean onHide; public void setInformationProvider(ITooltipInformationProvider provider) { this.fProvider = provider; } /* * @see IInformationPresenter#getInformationProvider(String) */ public ITooltipInformationProvider getInformationProvider() { return fProvider; } /* * @see AbstractInformationControlManager#computeInformation() */ protected void computeInformation() { if (fProvider == null) return; Object info = fProvider.getInformation(this.fControl); Point point = fProvider.getPosition(this.fControl); //the width and height are later calculated base on the size of the information to be shown. setInformation(info, new Rectangle(point.x, point.y, 0, 0)); } /* * @see IInformationPresenter#uninstall() */ public void uninstall() { dispose(); } /* * @see AbstractInformationControlManager#showInformationControl(Rectangle) */ protected void showInformationControl(Rectangle subjectArea) { if (fControl instanceof IWidgetTokenOwnerExtension && fControl instanceof IWidgetTokenOwner) { IWidgetTokenOwnerExtension extension = (IWidgetTokenOwnerExtension) fControl; if (extension.requestWidgetToken(this, WIDGET_PRIORITY)) { super.showInformationControl(subjectArea); } } else if (fControl instanceof IWidgetTokenOwner) { IWidgetTokenOwner owner = (IWidgetTokenOwner) fControl; if (owner.requestWidgetToken(this)) { super.showInformationControl(subjectArea); } } else { super.showInformationControl(subjectArea); } } @Override public void hideInformationControl() { hideInformationControl(false, true); } /* * @see AbstractInformationControlManager#hideInformationControl() */ public void hideInformationControl(boolean activateEditor, boolean restoreFocus) { //When hiding it may call hide again (because as it gets hidden our handlers are still connected). if (this.onHide) { return; } this.onHide = true; try { try { super.hideInformationControl(); } finally { if (fControl instanceof IWidgetTokenOwner) { IWidgetTokenOwner owner = (IWidgetTokenOwner) fControl; owner.releaseWidgetToken(this); } } this.disposeInformationControl(); //Restore previous active shell? if (this.fInitiallyActiveShell != null && !this.fInitiallyActiveShell.isDisposed()) { if (restoreFocus) { this.fInitiallyActiveShell.setActive(); } this.fInitiallyActiveShell = null; } if (this.fFocusControl != null && !this.fFocusControl.isDisposed()) { if (restoreFocus) { this.fFocusControl.setFocus(); } this.fFocusControl = null; } if (activateEditor) { KeyBindingHelper.executeCommand("org.eclipse.ui.window.activateEditor"); } } finally { this.onHide = false; } } /* * @see AbstractInformationControlManager#handleInformationControlDisposed() */ protected void handleInformationControlDisposed() { try { super.handleInformationControlDisposed(); } finally { if (fControl instanceof IWidgetTokenOwner) { IWidgetTokenOwner owner = (IWidgetTokenOwner) fControl; owner.releaseWidgetToken(this); } } } /* * @see org.eclipse.jface.text.IWidgetTokenKeeper#requestWidgetToken(IWidgetTokenOwner) */ public boolean requestWidgetToken(IWidgetTokenOwner owner) { return false; } /* * @see org.eclipse.jface.text.IWidgetTokenKeeperExtension#requestWidgetToken(org.eclipse.jface.text.IWidgetTokenOwner, int) * @since 3.0 */ public boolean requestWidgetToken(IWidgetTokenOwner owner, int priority) { return false; } /* * @see org.eclipse.jface.text.IWidgetTokenKeeperExtension#setFocus(org.eclipse.jface.text.IWidgetTokenOwner) * @since 3.0 */ public boolean setFocus(IWidgetTokenOwner owner) { return false; } /* (non-Javadoc) * @see org.python.pydev.core.tooltips.presenter.IInformationPresenterControlManager#setActivateEditorBinding(org.eclipse.jface.bindings.keys.KeySequence) */ public void setActivateEditorBinding(KeySequence activateEditorBinding) { fActivateEditorBinding = activateEditorBinding; } /* (non-Javadoc) * @see org.python.pydev.core.tooltips.presenter.IInformationPresenterControlManager#setInitiallyActiveShell(org.eclipse.swt.widgets.Shell) */ public void setInitiallyActiveShell(Shell activeShell) { this.fInitiallyActiveShell = activeShell; this.fFocusControl = null; if (activeShell != null) { Display display = activeShell.getDisplay(); if (display != null) { this.fFocusControl = display.getFocusControl(); } } } public String getTooltipAffordanceString() { if (tooltipAffordanceString != null) { return tooltipAffordanceString; } String defaultStr = "ESC to close, ENTER activate link."; if (this.fActivateEditorBinding != null) { return com.aptana.shared_core.string.StringUtils.format("%s to activate editor, %s", fActivateEditorBinding.toString(), defaultStr); } return defaultStr; } }