blob: 400b1d6a56a20a092a82455f5bb3dbb0a71d2375 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ecommons.ui.util;
import org.eclipse.jface.internal.text.InformationControlReplacer;
import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
import org.eclipse.jface.text.DefaultInformationControl;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IInformationControlExtension3;
import org.eclipse.jface.text.IInformationControlExtension5;
import org.eclipse.jface.text.IViewportListener;
import org.eclipse.jface.text.IWidgetTokenKeeper;
import org.eclipse.jface.text.IWidgetTokenKeeperExtension;
import org.eclipse.jface.text.IWidgetTokenOwner;
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.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;
/**
* Implements a sticky hover control, i.e. a control that replaces a hover
* with an enriched and focusable control.
* For ColumnViewer (instead of TextViewer).
* <p>
* The information control is made visible on request by calling
* {@link #showInformationControl(Rectangle)}.
* </p>
* <p>
* Clients usually instantiate and configure this class before using it. The configuration
* must be consistent: This means the used {@link org.eclipse.jface.text.IInformationControlCreator}
* must create an information control expecting information in the same format the configured
* {@link org.eclipse.jface.text.information.IInformationProvider}s use to encode the information they provide.
* </p>
*/
public class ColumnHoverStickyManager extends InformationControlReplacer implements IWidgetTokenKeeper, IWidgetTokenKeeperExtension {
/**
* Priority of the info controls managed by this sticky hover manager.
* <p>
* Note: Only applicable when info control does not have focus.
* -5 as value has been chosen in order to be beaten by the hovers of TextViewerHoverManager.
* </p>
*/
private 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, IViewportListener, KeyListener, FocusListener, Listener {
//TODO: Catch 'Esc' key in fInformationControlToClose: Don't dispose, just hideInformationControl().
// This would allow to reuse the information control also when the user explicitly closes it.
//TODO: if subject control is a Scrollable, should add selection listeners to both scroll bars
// (and remove the ViewPortListener, which only listens to vertical scrolling)
/** The subject control. */
private Control fSubjectControl;
/** Indicates whether this closer is active. */
private boolean fIsActive= false;
/** The display. */
private Display fDisplay;
@Override
public void setSubjectControl(final Control control) {
fSubjectControl= control;
}
@Override
public void setInformationControl(final IInformationControl control) {
// NOTE: we use getCurrentInformationControl2() from the outer class
}
@Override
public void start(final Rectangle informationArea) {
if (fIsActive) {
return;
}
fIsActive= true;
if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
fSubjectControl.addControlListener(this);
fSubjectControl.addMouseListener(this);
fSubjectControl.addKeyListener(this);
}
final IInformationControl fInformationControlToClose= getCurrentInformationControl2();
if (fInformationControlToClose != null) {
fInformationControlToClose.addFocusListener(this);
}
fDisplay= fSubjectControl.getDisplay();
if (!fDisplay.isDisposed()) {
fDisplay.addFilter(SWT.MouseMove, this);
fDisplay.addFilter(SWT.FocusOut, this);
}
}
@Override
public void stop() {
if (!fIsActive) {
return;
}
fIsActive= false;
if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
fSubjectControl.removeControlListener(this);
fSubjectControl.removeMouseListener(this);
fSubjectControl.removeKeyListener(this);
}
final IInformationControl fInformationControlToClose= getCurrentInformationControl2();
if (fInformationControlToClose != null) {
fInformationControlToClose.removeFocusListener(this);
}
if (fDisplay != null && !fDisplay.isDisposed()) {
fDisplay.removeFilter(SWT.MouseMove, this);
fDisplay.removeFilter(SWT.FocusOut, this);
}
fDisplay= null;
}
@Override
public void controlResized(final ControlEvent e) {
hideInformationControl();
}
@Override
public void controlMoved(final ControlEvent e) {
hideInformationControl();
}
@Override
public void mouseDown(final MouseEvent e) {
hideInformationControl();
}
@Override
public void mouseUp(final MouseEvent e) {
}
@Override
public void mouseDoubleClick(final MouseEvent e) {
hideInformationControl();
}
@Override
public void viewportChanged(final int topIndex) {
hideInformationControl();
}
@Override
public void keyPressed(final KeyEvent e) {
hideInformationControl();
}
@Override
public void keyReleased(final KeyEvent e) {
}
@Override
public void focusGained(final FocusEvent e) {
}
@Override
public void focusLost(final FocusEvent e) {
if (DEBUG) {
System.out.println("StickyHoverManager.Closer.focusLost(): " + e); //$NON-NLS-1$
}
final Display d= fSubjectControl.getDisplay();
d.asyncExec(new Runnable() {
// Without the asyncExec, mouse clicks to the workbench window are swallowed.
@Override
public void run() {
hideInformationControl();
}
});
}
@Override
public void handleEvent(final Event event) {
if (event.type == SWT.MouseMove) {
if (!(event.widget instanceof Control) || event.widget.isDisposed()) {
return;
}
final IInformationControl infoControl= getCurrentInformationControl2();
if (infoControl != null && !infoControl.isFocusControl() && infoControl instanceof IInformationControlExtension3) {
// if (DEBUG) System.out.println("StickyHoverManager.Closer.handleEvent(): activeShell= " + fDisplay.getActiveShell()); //$NON-NLS-1$
final IInformationControlExtension3 iControl3= (IInformationControlExtension3) infoControl;
final Rectangle controlBounds= iControl3.getBounds();
if (controlBounds != null) {
final Point mouseLoc= event.display.map((Control) event.widget, null, event.x, event.y);
final int margin= getKeepUpMargin();
Geometry.expand(controlBounds, margin, margin, margin, margin);
if (!controlBounds.contains(mouseLoc)) {
hideInformationControl();
}
}
}
else {
/*
* TODO: need better understanding of why/if this is needed.
* Looks like the same panic code we have in org.eclipse.jface.text.AbstractHoverInformationControlManager.Closer.handleMouseMove(Event)
*/
if (fDisplay != null && !fDisplay.isDisposed()) {
fDisplay.removeFilter(SWT.MouseMove, this);
}
}
}
else if (event.type == SWT.FocusOut) {
if (DEBUG) {
System.out.println("StickyHoverManager.Closer.handleEvent(): focusOut: " + event); //$NON-NLS-1$
}
final IInformationControl iControl= getCurrentInformationControl2();
if (iControl != null && ! iControl.isFocusControl()) {
hideInformationControl();
}
}
}
}
private final ColumnWidgetTokenOwner fTokenOwner;
private IWidgetTokenKeeper fTokenKeeperToReplace;
/**
* Creates a new sticky hover manager.
*
* @param textViewer the text viewer
*/
public ColumnHoverStickyManager(final ColumnWidgetTokenOwner owner, final IWidgetTokenKeeper keeperToReplace) {
super(new AbstractReusableInformationControlCreator() {
@Override
public IInformationControl doCreateInformationControl(final Shell shell) {
return new DefaultInformationControl(shell, true);
}
});
fTokenOwner = owner;
fTokenKeeperToReplace = keeperToReplace;
setCloser(new Closer());
install(fTokenOwner.getControl());
}
@Override
protected void showInformationControl(final Rectangle subjectArea) {
if (fTokenOwner != null && fTokenOwner.requestWidgetToken(this, WIDGET_PRIORITY)) {
super.showInformationControl(subjectArea);
} else
if (DEBUG) {
System.out.println("cancelled StickyHoverManager.showInformationControl(..): did not get widget token (with prio)"); //$NON-NLS-1$
}
}
@Override
public void hideInformationControl() {
try {
super.hideInformationControl();
} finally {
if (fTokenOwner != null) {
fTokenOwner.releaseWidgetToken(this);
}
}
}
@Override
protected void handleInformationControlDisposed() {
try {
super.handleInformationControlDisposed();
} finally {
if (fTokenOwner != null) {
fTokenOwner.releaseWidgetToken(this);
}
}
}
@Override
public void replaceInformationControl(final IInformationControlCreator informationPresenterControlCreator,
final Rectangle contentBounds, final Object information, final Rectangle subjectArea, final boolean takeFocus) {
if (fTokenOwner != null && fTokenKeeperToReplace != null) {
fTokenOwner.releaseWidgetToken(fTokenKeeperToReplace);
}
super.replaceInformationControl(informationPresenterControlCreator, contentBounds, information, subjectArea, takeFocus);
}
@Override
public boolean requestWidgetToken(final IWidgetTokenOwner owner) {
hideInformationControl();
if (DEBUG) {
System.out.println("StickyHoverManager gave up widget token (no prio)"); //$NON-NLS-1$
}
return true;
}
@Override
public boolean requestWidgetToken(final IWidgetTokenOwner owner, final int priority) {
if (getCurrentInformationControl2() != null) {
if (getCurrentInformationControl2().isFocusControl()) {
if (DEBUG) {
System.out.println("StickyHoverManager kept widget token (focused)"); //$NON-NLS-1$
}
return false;
} else if (priority > WIDGET_PRIORITY) {
hideInformationControl();
if (DEBUG) {
System.out.println("StickyHoverManager gave up widget token (prio)"); //$NON-NLS-1$
}
return true;
} else {
if (DEBUG) {
System.out.println("StickyHoverManager kept widget token (prio)"); //$NON-NLS-1$
}
return false;
}
}
if (DEBUG) {
System.out.println("StickyHoverManager gave up widget token (no iControl)"); //$NON-NLS-1$
}
return true;
}
@Override
public boolean setFocus(final IWidgetTokenOwner owner) {
final IInformationControl iControl= getCurrentInformationControl2();
if (iControl instanceof IInformationControlExtension5) {
final IInformationControlExtension5 iControl5= (IInformationControlExtension5) iControl;
if (iControl5.isVisible()) {
iControl.setFocus();
return iControl.isFocusControl();
}
return false;
}
iControl.setFocus();
return iControl.isFocusControl();
}
}