blob: 2a9a5b05714f8a72941e7c86d52e0a6b5e4506eb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2016 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;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Canvas;
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.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.internal.text.revisions.Colors;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Geometry;
/**
* An abstract information control that can show content inside a shell.
* The information control can be created in two styles:
* <ul>
* <li>non-resizable tooltip with optional status</li>
* <li>resizable tooltip with optional tool bar</li>
* </ul>
* Additionally it can present either a status line containing a status text or
* a toolbar containing toolbar buttons.
* <p>
* Subclasses must either override {@link IInformationControl#setInformation(String)}
* or implement {@link IInformationControlExtension2}.
* They should also extend {@link #computeTrim()} if they create a content area
* with additional trim (e.g. scrollbars) and override {@link #getInformationPresenterControlCreator()}.
* </p>
*
* @since 3.4
*/
public abstract class AbstractInformationControl implements IInformationControl, IInformationControlExtension, IInformationControlExtension3, IInformationControlExtension4, IInformationControlExtension5 {
/** The information control's shell. */
private final Shell fShell;
/** Composite containing the content created by subclasses. */
private final Composite fContentComposite;
/** Whether the information control is resizable. */
private final boolean fResizable;
/** Composite containing the status line content or <code>null</code> if none. */
private Composite fStatusComposite;
/** Separator between content and status line or <code>null</code> if none. */
private Label fSeparator;
/** Label in the status line or <code>null</code> if none. */
private Label fStatusLabel;
/**
* Font for the label in the status line or <code>null</code> if none.
* @since 3.4.2
*/
private Font fStatusLabelFont;
/**
* Color for the label in the status line or <code>null</code> if none.
*
* @since 3.6
*/
private Color fStatusLabelForeground;
/** The toolbar manager used by the toolbar or <code>null</code> if none. */
private final ToolBarManager fToolBarManager;
/** Status line toolbar or <code>null</code> if none. */
private ToolBar fToolBar;
/** Listener for shell activation and deactivation. */
private Listener fShellListener;
/** All focus listeners registered to this information control. */
private final ListenerList<FocusListener> fFocusListeners= new ListenerList<>(ListenerList.IDENTITY);
/** Size constraints, x is the maxWidth and y is the maxHeight, or <code>null</code> if not set. */
private Point fSizeConstraints;
/** The size of the resize handle if already set, -1 otherwise */
private int fResizeHandleSize;
/**
* Creates an abstract information control with the given shell as parent.
* The control will not be resizable and optionally show a status line with
* the given status field text.
* <p>
* <em>Important: Subclasses are required to call {@link #create()} at the end of their constructor.</em>
* </p>
*
* @param parentShell the parent of this control's shell
* @param statusFieldText the text to be used in the status field or <code>null</code> to hide the status field
*/
public AbstractInformationControl(Shell parentShell, String statusFieldText) {
this(parentShell, SWT.TOOL | SWT.ON_TOP, statusFieldText, null);
}
/**
* Creates an abstract information control with the given shell as parent.
* The control will be resizable and optionally show a tool bar managed by
* the given tool bar manager.
* <p>
* <em>Important: Subclasses are required to call {@link #create()} at the end of their constructor.</em>
* </p>
*
* @param parentShell the parent of this control's shell
* @param toolBarManager the manager or <code>null</code> if toolbar is not desired
*/
public AbstractInformationControl(Shell parentShell, ToolBarManager toolBarManager) {
this(parentShell, SWT.TOOL | SWT.ON_TOP | SWT.RESIZE, null, toolBarManager);
}
/**
* Creates an abstract information control with the given shell as parent.
* <p>
* <em>Important: Subclasses are required to call {@link #create()} at the end of their constructor.</em>
* </p>
*
* @param parentShell the parent of this control's shell
* @param isResizable <code>true</code> if the control should be resizable
*/
public AbstractInformationControl(Shell parentShell, boolean isResizable) {
this(parentShell, SWT.TOOL | SWT.ON_TOP | (isResizable ? SWT.RESIZE : 0), null, null);
}
/**
* Creates an abstract information control with the given shell as parent.
* The given shell style is used for the shell (NO_TRIM will be removed to make sure there's a border).
* <p>
* The control will optionally show either a status line or a tool bar.
* At most one of <code>toolBarManager</code> or <code>statusFieldText</code> can be non-null.
* </p>
* <p>
* <strong>Important:</strong>: Subclasses are required to call {@link #create()} at the end of their constructor.
* </p>
*
* @param parentShell the parent of this control's shell
* @param shellStyle style of this control's shell
* @param statusFieldText the text to be used in the status field or <code>null</code> to hide the status field
* @param toolBarManager the manager or <code>null</code> if toolbar is not desired
*
* @deprecated clients should use one of the public constructors
*/
@Deprecated
AbstractInformationControl(Shell parentShell, int shellStyle, final String statusFieldText, final ToolBarManager toolBarManager) {
Assert.isTrue(statusFieldText == null || toolBarManager == null);
fResizeHandleSize= -1;
fToolBarManager= toolBarManager;
if ((shellStyle & SWT.NO_TRIM) != 0)
shellStyle&= ~(SWT.NO_TRIM | SWT.SHELL_TRIM); // make sure we get the OS border but no other trims
fResizable= (shellStyle & SWT.RESIZE) != 0;
fShell= new Shell(parentShell, shellStyle);
Display display= fShell.getDisplay();
Color foreground= JFaceColors.getInformationViewerForegroundColor(display);
Color background= JFaceColors.getInformationViewerBackgroundColor(display);
setColor(fShell, foreground, background);
GridLayout layout= new GridLayout(1, false);
layout.marginHeight= 0;
layout.marginWidth= 0;
layout.verticalSpacing= 0;
fShell.setLayout(layout);
fContentComposite= new Composite(fShell, SWT.NONE);
fContentComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
fContentComposite.setLayout(new FillLayout());
setColor(fContentComposite, foreground, background);
createStatusComposite(statusFieldText, toolBarManager, foreground, background);
addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
handleDispose();
}
});
}
private void createStatusComposite(final String statusFieldText, final ToolBarManager toolBarManager, Color foreground, Color background) {
if (toolBarManager == null && statusFieldText == null)
return;
fStatusComposite= new Composite(fShell, SWT.NONE);
GridData gridData= new GridData(SWT.FILL, SWT.BOTTOM, true, false);
fStatusComposite.setLayoutData(gridData);
GridLayout statusLayout= new GridLayout(1, false);
statusLayout.marginHeight= 0;
statusLayout.marginWidth= 0;
statusLayout.verticalSpacing= 1;
fStatusComposite.setLayout(statusLayout);
fSeparator= new Label(fStatusComposite, SWT.SEPARATOR | SWT.HORIZONTAL);
fSeparator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
if (statusFieldText != null) {
createStatusLabel(statusFieldText, foreground, background);
} else {
createToolBar(toolBarManager);
}
}
private void createStatusLabel(final String statusFieldText, Color foreground, Color background) {
fStatusLabel= new Label(fStatusComposite, SWT.RIGHT);
fStatusLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
fStatusLabel.setText(statusFieldText);
FontData[] fontDatas= JFaceResources.getDialogFont().getFontData();
for (FontData fontData : fontDatas) {
fontData.setHeight(fontData.getHeight() * 9 / 10);
}
fStatusLabelFont= new Font(fStatusLabel.getDisplay(), fontDatas);
fStatusLabel.setFont(fStatusLabelFont);
fStatusLabelForeground= new Color(fStatusLabel.getDisplay(), Colors.blend(background.getRGB(), foreground.getRGB(), 0.56f));
setColor(fStatusLabel, fStatusLabelForeground, background);
setColor(fStatusComposite, foreground, background);
}
private void createToolBar(ToolBarManager toolBarManager) {
final Composite bars= new Composite(fStatusComposite, SWT.NONE);
bars.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
GridLayout layout= new GridLayout(3, false);
layout.marginHeight= 0;
layout.marginWidth= 0;
layout.horizontalSpacing= 0;
layout.verticalSpacing= 0;
bars.setLayout(layout);
fToolBar= toolBarManager.createControl(bars);
GridData gd= new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false);
fToolBar.setLayoutData(gd);
Composite spacer= new Composite(bars, SWT.NONE);
gd= new GridData(SWT.FILL, SWT.FILL, true, true);
gd.widthHint= 0;
gd.heightHint= 0;
spacer.setLayoutData(gd);
addMoveSupport(spacer);
addResizeSupportIfNecessary(bars);
}
private void addResizeSupportIfNecessary(final Composite bars) {
// XXX: workarounds for
// - https://bugs.eclipse.org/bugs/show_bug.cgi?id=219139 : API to add resize grip / grow box in lower right corner of shell
// - https://bugs.eclipse.org/bugs/show_bug.cgi?id=23980 : platform specific shell resize behavior
String platform= SWT.getPlatform();
final boolean isWin= platform.equals("win32"); //$NON-NLS-1$
if (!isWin && !platform.equals("gtk")) //$NON-NLS-1$
return;
final Canvas resizer= new Canvas(bars, SWT.NONE);
int size= getResizeHandleSize(bars);
GridData data= new GridData(SWT.END, SWT.END, false, true);
data.widthHint= size;
data.heightHint= size;
resizer.setLayoutData(data);
resizer.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
Point s= resizer.getSize();
int x= s.x - 2;
int y= s.y - 2;
int min= Math.min(x, y);
if (isWin) {
// draw dots
e.gc.setBackground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW));
int end= min - 1;
for (int i= 0; i <= 2; i++)
for (int j= 0; j <= 2 - i; j++)
e.gc.fillRectangle(end - 4 * i, end - 4 * j, 2, 2);
end--;
e.gc.setBackground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
for (int i= 0; i <= 2; i++)
for (int j= 0; j <= 2 - i; j++)
e.gc.fillRectangle(end - 4 * i, end - 4 * j, 2, 2);
} else {
// draw diagonal lines
e.gc.setForeground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
for (int i= 1; i < min; i+= 4) {
e.gc.drawLine(i, y, x, i);
}
e.gc.setForeground(resizer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW));
for (int i= 2; i < min; i+= 4) {
e.gc.drawLine(i, y, x, i);
}
}
}
});
final boolean isRTL= (resizer.getShell().getStyle() & SWT.RIGHT_TO_LEFT) != 0;
resizer.setCursor(resizer.getDisplay().getSystemCursor(isRTL ? SWT.CURSOR_SIZESW : SWT.CURSOR_SIZESE));
MouseAdapter resizeSupport= new MouseAdapter() {
private MouseMoveListener fResizeListener;
@Override
public void mouseDown(MouseEvent e) {
Rectangle shellBounds= fShell.getBounds();
final int shellX= shellBounds.x;
final int shellY= shellBounds.y;
final int shellWidth= shellBounds.width;
final int shellHeight= shellBounds.height;
Point mouseLoc= resizer.toDisplay(e.x, e.y);
final int mouseX= mouseLoc.x;
final int mouseY= mouseLoc.y;
fResizeListener= new MouseMoveListener() {
@Override
public void mouseMove(MouseEvent e2) {
Point mouseLoc2= resizer.toDisplay(e2.x, e2.y);
int dx= mouseLoc2.x - mouseX;
int dy= mouseLoc2.y - mouseY;
if (isRTL) {
setLocation(new Point(shellX + dx, shellY));
setSize(shellWidth - dx, shellHeight + dy);
} else {
setSize(shellWidth + dx, shellHeight + dy);
}
}
};
resizer.addMouseMoveListener(fResizeListener);
}
@Override
public void mouseUp(MouseEvent e) {
resizer.removeMouseMoveListener(fResizeListener);
fResizeListener= null;
}
};
resizer.addMouseListener(resizeSupport);
}
private int getResizeHandleSize(Composite parent) {
if (fResizeHandleSize == -1) {
Slider sliderV= new Slider(parent, SWT.VERTICAL);
Slider sliderH= new Slider(parent, SWT.HORIZONTAL);
int width= sliderV.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
int height= sliderH.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
sliderV.dispose();
sliderH.dispose();
fResizeHandleSize= Math.min(width, height);
}
return fResizeHandleSize;
}
/**
* Adds support to move the shell by dragging the given control.
*
* @param control the control that can be used to move the shell
*/
private void addMoveSupport(final Control control) {
MouseAdapter moveSupport= new MouseAdapter() {
private MouseMoveListener fMoveListener;
@Override
public void mouseDown(MouseEvent e) {
Point shellLoc= fShell.getLocation();
final int shellX= shellLoc.x;
final int shellY= shellLoc.y;
Point mouseLoc= control.toDisplay(e.x, e.y);
final int mouseX= mouseLoc.x;
final int mouseY= mouseLoc.y;
fMoveListener= new MouseMoveListener() {
@Override
public void mouseMove(MouseEvent e2) {
Point mouseLoc2= control.toDisplay(e2.x, e2.y);
int dx= mouseLoc2.x - mouseX;
int dy= mouseLoc2.y - mouseY;
fShell.setLocation(shellX + dx, shellY + dy);
}
};
control.addMouseMoveListener(fMoveListener);
}
@Override
public void mouseUp(MouseEvent e) {
control.removeMouseMoveListener(fMoveListener);
fMoveListener= null;
}
};
control.addMouseListener(moveSupport);
}
/**
* Utility to set the foreground and the background color of the given
* control
*
* @param control the control to modify
* @param foreground the color to use for the foreground
* @param background the color to use for the background
*/
private static void setColor(Control control, Color foreground, Color background) {
control.setForeground(foreground);
control.setBackground(background);
}
/**
* The shell of the popup window.
*
* @return the shell used for the popup window
*/
protected final Shell getShell() {
return fShell;
}
/**
* The toolbar manager used to manage the toolbar, or <code>null</code> if
* no toolbar is shown.
*
* @return the tool bar manager or <code>null</code>
*/
protected final ToolBarManager getToolBarManager() {
return fToolBarManager;
}
/**
* Creates the content of this information control. Subclasses must call
* this method at the end of their constructor(s).
*/
protected final void create() {
createContent(fContentComposite);
}
/**
* Creates the content of the popup window.
* <p>
* Implementors will usually take over {@link Composite#getBackground()} and
* {@link Composite#getForeground()} from <code>parent</code>.
* </p>
* <p>
* Implementors must either use the dialog font or override
* {@link #computeSizeConstraints(int, int)}.
* </p>
* <p>
* Implementors are expected to consider {@link #isResizable()}: If <code>true</code>, they
* should show scrollbars if their content may exceed the size of the information control. If
* <code>false</code>, they should never show scrollbars.
* </p>
* <p>
* The given <code>parent</code> comes with a {@link FillLayout}. Subclasses may set a different
* layout.
* </p>
*
* @param parent the container of the content
*/
protected abstract void createContent(Composite parent);
/**
* Sets the information to be presented by this information control.
* <p>
* The default implementation does nothing. Subclasses must either override this method
* or implement {@link IInformationControlExtension2}.
*
* @param information the information to be presented
*
* @see org.eclipse.jface.text.IInformationControl#setInformation(java.lang.String)
*/
@Override
public void setInformation(String information) {
}
/**
* Returns whether the information control is resizable.
*
* @return <code>true</code> if the information control is resizable,
* <code>false</code> if it is not resizable.
*/
public boolean isResizable() {
return fResizable;
}
@Override
public void setVisible(boolean visible) {
if (fShell.isVisible() == visible)
return;
fShell.setVisible(visible);
}
@Override
public void dispose() {
if (fShell != null && !fShell.isDisposed())
fShell.dispose();
}
/**
* Frees all resources allocated by this information control. Internally called when the
* information control's shell has been disposed.
*
* @since 3.6
*/
protected void handleDispose() {
if (fStatusLabelFont != null) {
fStatusLabelFont.dispose();
fStatusLabelFont= null;
}
if (fStatusLabelForeground != null) {
fStatusLabelForeground.dispose();
fStatusLabelForeground= null;
}
}
@Override
public void setSize(int width, int height) {
fShell.setSize(width, height);
}
@Override
public void setLocation(Point location) {
fShell.setLocation(location);
}
@Override
public void setSizeConstraints(int maxWidth, int maxHeight) {
fSizeConstraints= new Point(maxWidth, maxHeight);
}
/**
* Returns the size constraints.
*
* @return the size constraints or <code>null</code> if not set
* @see #setSizeConstraints(int, int)
*/
protected final Point getSizeConstraints() {
return fSizeConstraints != null ? Geometry.copy(fSizeConstraints) : null;
}
@Override
public Point computeSizeHint() {
// XXX: Verify whether this is a good default implementation. If yes, document it.
Point constrains= getSizeConstraints();
if (constrains == null)
return fShell.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
return fShell.computeSize(constrains.x, constrains.y, true);
}
/**
* Computes the trim (status text and tool bar are considered as trim).
* Subclasses can extend this method to add additional trim (e.g. scroll
* bars for resizable information controls).
*
* @see org.eclipse.jface.text.IInformationControlExtension3#computeTrim()
*/
@Override
public Rectangle computeTrim() {
Rectangle trim= fShell.computeTrim(0, 0, 0, 0);
if (fStatusComposite != null)
trim.height+= fStatusComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
return trim;
}
@Override
public Rectangle getBounds() {
return fShell.getBounds();
}
/**
* {@inheritDoc}
* <p>
* The default implementation always returns <code>false</code>.
* </p>
* @see org.eclipse.jface.text.IInformationControlExtension3#restoresLocation()
*/
@Override
public boolean restoresLocation() {
return false;
}
/**
* {@inheritDoc}
* <p>
* The default implementation always returns <code>false</code>.
* </p>
* @see org.eclipse.jface.text.IInformationControlExtension3#restoresSize()
*/
@Override
public boolean restoresSize() {
return false;
}
@Override
public void addDisposeListener(DisposeListener listener) {
fShell.addDisposeListener(listener);
}
@Override
public void removeDisposeListener(DisposeListener listener) {
fShell.removeDisposeListener(listener);
}
@Override
public void setForegroundColor(Color foreground) {
fContentComposite.setForeground(foreground);
}
@Override
public void setBackgroundColor(Color background) {
fContentComposite.setBackground(background);
}
/**
* {@inheritDoc}
* This method is not intended to be overridden by subclasses.
*/
@Override
public boolean isFocusControl() {
return fShell.getDisplay().getActiveShell() == fShell;
}
/**
* This default implementation sets the focus on the popup shell.
* Subclasses can override or extend.
*
* @see IInformationControl#setFocus()
*/
@Override
public void setFocus() {
boolean focusTaken= fShell.setFocus();
if (!focusTaken)
fShell.forceFocus();
}
/**
* {@inheritDoc}
* This method is not intended to be overridden by subclasses.
*/
@Override
public void addFocusListener(final FocusListener listener) {
if (fFocusListeners.isEmpty()) {
fShellListener= new Listener() {
@Override
public void handleEvent(Event event) {
for (FocusListener focusListener : fFocusListeners) {
if (event.type == SWT.Activate) {
focusListener.focusGained(new FocusEvent(event));
} else {
focusListener.focusLost(new FocusEvent(event));
}
}
}
};
fShell.addListener(SWT.Deactivate, fShellListener);
fShell.addListener(SWT.Activate, fShellListener);
}
fFocusListeners.add(listener);
}
/**
* {@inheritDoc}
* This method is not intended to be overridden by subclasses.
*/
@Override
public void removeFocusListener(FocusListener listener) {
fFocusListeners.remove(listener);
if (fFocusListeners.isEmpty()) {
fShell.removeListener(SWT.Activate, fShellListener);
fShell.removeListener(SWT.Deactivate, fShellListener);
fShellListener= null;
}
}
/**
* Sets the text of the status field.
* <p>
* The default implementation currently only updates the status field when
* the popup shell is not visible. The status field can currently only be
* shown if the information control has been created with a non-null status
* field text.
* </p>
*
* @param statusFieldText the text to be used in the optional status field
* or <code>null</code> if the status field should be hidden
*
* @see org.eclipse.jface.text.IInformationControlExtension4#setStatusText(java.lang.String)
*/
@Override
public void setStatusText(String statusFieldText) {
if (fStatusLabel != null && ! getShell().isVisible()) {
if (statusFieldText == null ) {
fStatusComposite.setVisible(false);
} else {
fStatusLabel.setText(statusFieldText);
fStatusComposite.setVisible(true);
}
}
}
@Override
public boolean containsControl(Control control) {
do {
if (control == fShell)
return true;
if (control instanceof Shell)
return false;
control= control.getParent();
} while (control != null);
return false;
}
@Override
public boolean isVisible() {
return fShell != null && !fShell.isDisposed() && fShell.isVisible();
}
/**
* {@inheritDoc}
* This default implementation returns <code>null</code>. Subclasses may override.
*/
@Override
public IInformationControlCreator getInformationPresenterControlCreator() {
return null;
}
/**
* Computes the size constraints based on the
* {@link JFaceResources#getDialogFont() dialog font}. Subclasses can
* override or extend.
*
* @see org.eclipse.jface.text.IInformationControlExtension5#computeSizeConstraints(int, int)
*/
@Override
public Point computeSizeConstraints(int widthInChars, int heightInChars) {
GC gc= new GC(fContentComposite);
gc.setFont(JFaceResources.getDialogFont());
int width= gc.getFontMetrics().getAverageCharWidth();
int height= gc.getFontMetrics().getHeight();
gc.dispose();
return new Point(widthInChars * width, heightInChars * height);
}
}