blob: 88b3924ed4e480fb0c0c67209625c1f9ef5552d9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005 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.fieldassist;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
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.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Region;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
/**
* DecoratedField manages image decorations around a control. It allows clients
* to specify an image decoration and a position for the decoration relative to
* the field. Decorations may be assigned descriptions, which are shown when the
* user hovers over the decoration. Clients can decorate any kind of control by
* supplying a {@link IControlCreator} to create the control that is decorated.
*
* <p>
* This API is considered experimental. It is still evolving during 3.2 and is
* subject to change. It is being released to obtain feedback from early
* adopters.
*
* @since 3.2
*/
public class DecoratedField {
/**
* Number of pixels to reserve for decorations.
*/
private static int RESERVED_WIDTH = 8;
/**
* Cached platform flags for dealing with platform-specific issues.
*/
private static boolean CARBON = "carbon".equals(SWT.getPlatform()); //$NON-NLS-1$
/**
* Constants describing the array indices used to hold the decorations in
* array slots.
*/
private static final int LEFT_TOP = 0;
private static final int LEFT_BOTTOM = 1;
private static final int RIGHT_TOP = 2;
private static final int RIGHT_BOTTOM = 3;
private static final int DECORATION_SLOTS = 4;
/**
* Get the width (in pixels) that should always be reserved for field
* decorations, regardless of the actual width of any supplied decorations.
* This value is used as the minimum width for any decorations that have
* been added to the field, and can be used by clients to compute margins in
* order to align non-decorated fields with decorated fields.
*
* @return decorationWidth the width in pixels reserved for decorations
*/
public static int getReservedDecorationWidth() {
return RESERVED_WIDTH;
}
/**
* Set the width (in pixels) that should always be reserved for field
* decorations, regardless of the actual width of any supplied decorations.
* Field alignment within dialogs will look best when all decorations
* supplied conform to the reserved width. However, the field decoration
* area for a particular field will be expanded if decorations larger than
* the reserved width are supplied.
*
* @param decorationWidth
* the width in pixels reserved for decorations
*/
public static void setReservedDecorationWidth(int decorationWidth) {
RESERVED_WIDTH = decorationWidth;
}
/**
* Simple data structure class for specifying the internals for a field
* decoration. This class contains data specific to the implementation of
* field decorations as labels attached to the field. Clients should use
* <code>FieldDecoration</code> for specifying a decoration.
*/
private class FieldDecorationData {
/* Package */FieldDecoration decoration;
/* Package */Label label;
/* Package */FormData data;
/* Package */boolean showOnFocus;
/* Package */boolean visible = true;
/**
* Create a decoration data representing the specified decoration, using
* the specified label and form data for its representation.
*
* @param decoration
* the decoration whose data is kept.
* @param label
* the label used to represent the decoration.
* @param formData
* the form data used to attach the decoration to its field.
* @param showOnFocus
* a boolean specifying whether the decoration should only be
* shown when the field has focus.
*/
FieldDecorationData(FieldDecoration decoration, Label label,
FormData formData, boolean showOnFocus) {
this.decoration = decoration;
this.label = label;
this.data = formData;
this.showOnFocus = showOnFocus;
}
}
/**
* Decorations keyed by position.
*/
private FieldDecorationData[] decDatas = new FieldDecorationData[DECORATION_SLOTS];
/**
* The associated control
*/
private Control control;
/**
* The composite with form layout used to manage decorations.
*/
private Composite form;
/**
* The hover used for showing description text
*/
private Hover hover;
/**
* The hover used to show a decoration image's description.
*/
class Hover {
private static final String EMPTY = ""; //$NON-NLS-1$
/**
* Offset of info hover arrow from the left or right side.
*/
private int hao = 10;
/**
* Width of info hover arrow.
*/
private int haw = 8;
/**
* Height of info hover arrow.
*/
private int hah = 10;
/**
* Margin around info hover text.
*/
private int hm = 2;
/**
* This info hover's shell.
*/
Shell hoverShell;
/**
* The info hover text.
*/
String text = EMPTY;
/**
* The region used to manage the shell shape
*/
Region region;
/**
* Boolean indicating whether the last computed polygon location had an
* arrow on left. (true if left, false if right).
*/
boolean arrowOnLeft = true;
/*
* Create a hover parented by the specified shell.
*/
Hover(Shell parent) {
final Display display = parent.getDisplay();
hoverShell = new Shell(parent, SWT.NO_TRIM | SWT.ON_TOP
| SWT.NO_FOCUS);
hoverShell.setBackground(display
.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
hoverShell.setForeground(display
.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
hoverShell.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent pe) {
pe.gc.drawString(text, hm, hm);
if (!CARBON)
pe.gc.drawPolygon(getPolygon(true));
}
});
hoverShell.addMouseListener(new MouseAdapter() {
public void mouseDown(MouseEvent e) {
hideHover();
}
});
}
/*
* Compute a polygon that represents a hover with an arrow pointer. If
* border is true, compute the polygon inset by 1-pixel border. Consult
* the arrowOnLeft flag to determine which side the arrow is on.
*/
int[] getPolygon(boolean border) {
Point e = getExtent();
int b = border ? 1 : 0;
if (arrowOnLeft)
return new int[] { 0, 0, e.x - b, 0, e.x - b, e.y - b,
hao + haw, e.y - b, hao + haw / 2, e.y + hah - b, hao,
e.y - b, 0, e.y - b, 0, 0 };
return new int[] { 0, 0, e.x - b, 0, e.x - b, e.y - b,
e.x - hao - b, e.y - b, e.x - hao - haw / 2, e.y + hah - b,
e.x - hao - haw, e.y - b, 0, e.y - b, 0, 0 };
}
/*
* Dispose the hover, it is no longer needed. Dispose any resources
* allocated by the hover.
*/
void dispose() {
if (!hoverShell.isDisposed())
hoverShell.dispose();
if (region != null) {
region.dispose();
}
}
/*
* Set the visibility of the hover.
*/
void setVisible(boolean visible) {
if (visible) {
if (!hoverShell.isVisible())
hoverShell.setVisible(true);
} else {
if (hoverShell.isVisible())
hoverShell.setVisible(false);
}
}
/*
* Set the text of the hover to the specified text. Recompute the size
* and location of the hover to hover near the specified control,
* pointing the arrow toward the target control.
*/
void setText(String t, Control hoverNear, Control targetControl) {
if (t == null)
t = EMPTY;
if (!t.equals(text)) {
Point oldSize = getExtent();
text = t;
hoverShell.redraw();
Point newSize = getExtent();
if (!oldSize.equals(newSize)) {
// set a flag that indicates the direction of arrow
arrowOnLeft = hoverNear.getLocation().x <= targetControl
.getLocation().x;
setNewShape();
}
}
if (hoverNear != null) {
Point extent = getExtent();
int y = -extent.y - hah + 1;
int x = arrowOnLeft ? -hao + haw / 2 : -extent.x + hao + haw
/ 2;
hoverShell.setLocation(hoverNear.toDisplay(x, y));
}
}
/*
* Return whether or not the hover (shell) is visible.
*/
boolean isVisible() {
return hoverShell.isVisible();
}
/*
* Compute the extent of the hover for the current text.
*/
Point getExtent() {
GC gc = new GC(hoverShell);
Point e = gc.textExtent(text);
gc.dispose();
e.x += hm * 2;
e.y += hm * 2;
return e;
}
/*
* Compute a new shape for the hover shell.
*/
void setNewShape() {
Region oldRegion = region;
region = new Region();
region.add(getPolygon(false));
hoverShell.setRegion(region);
if (oldRegion != null) {
oldRegion.dispose();
}
}
}
/**
* Construct a decorated field which is parented by the specified composite
* and has the given style bits. Use the controlCreator to create the
* specific kind of control that is decorated inside the field.
*
* @param parent
* the parent of the decorated field.
* @param style
* the desired style bits for the field.
* @param controlCreator
* the IControlCreator used to specify the specific kind of
* control that is to be decorated.
*
* @see IControlCreator
*/
public DecoratedField(Composite parent, int style,
IControlCreator controlCreator) {
this.form = createForm(parent);
this.control = controlCreator.createControl(form, style);
addControlListeners();
// Add a dummy decoration on each side to reserve the width needed.
addFieldDecoration(new FieldDecoration(null, null), SWT.LEFT | SWT.TOP,
true);
addFieldDecoration(new FieldDecoration(null, null),
SWT.RIGHT | SWT.TOP, true);
form.setTabList(new Control[] { control });
// Set up the preferred width of the control and attachments to the
// decorations.
FormData data = new FormData();
GC gc = new GC(control);
gc.setFont(control.getFont());
FontMetrics fontMetrics = gc.getFontMetrics();
gc.dispose();
data.width = Dialog.convertHorizontalDLUsToPixels(fontMetrics,
IDialogConstants.ENTRY_FIELD_WIDTH)
+ 2 * RESERVED_WIDTH;
data.left = new FormAttachment(decDatas[LEFT_TOP].label);
data.right = new FormAttachment(decDatas[RIGHT_TOP].label);
data.top = new FormAttachment(0, 0);
control.setLayoutData(data);
}
/**
* Adds an image decoration to the field.
*
* @param decoration
* A FieldDecoration describing the image and description for the
* decoration
*
* @param position
* The SWT constant indicating the position of the decoration
* relative to the field's control. The position should include
* style bits describing both the vertical and horizontal
* orientation. <code>SWT.LEFT</code> and
* <code>SWT.RIGHT</code> describe the horizontal placement of
* the decoration relative to the field, and the constants
* <code>SWT.TOP</code> and <code>SWT.BOTTOM</code> describe
* the vertical alignment of the decoration relative to the
* field. Decorations always appear on either horizontal side of
* the field, never above or below it. For example, a decoration
* appearing on the left side of the field, at the top, is
* specified as SWT.LEFT | SWT.TOP. If an image decoration
* already exists in the specified position, it will be replaced
* by the one specified.
* @param showOnFocus
* <code>true</code> if the decoration should only be shown
* when the associated control has focus, <code>false</code> if
* it should always be shown.
*
*/
public void addFieldDecoration(FieldDecoration decoration, int position,
boolean showOnFocus) {
final Label label;
FormData formData;
int i = indexForPosition(position);
if (decDatas[i] == null) {
formData = createFormDataForIndex(i);
label = new Label(form, SWT.HORIZONTAL | SWT.VERTICAL | SWT.CENTER);
label.addMouseTrackListener(new MouseTrackListener() {
public void mouseHover(MouseEvent event) {
FieldDecorationData decData = (FieldDecorationData) event.widget
.getData();
String desc = decData.decoration.getDescription();
if (desc != null) {
showHoverText(desc, label);
}
}
public void mouseEnter(MouseEvent event) {
}
public void mouseExit(MouseEvent event) {
hideHover();
}
});
decDatas[i] = new FieldDecorationData(decoration, label, formData,
showOnFocus);
} else {
label = decDatas[i].label;
formData = decDatas[i].data;
decDatas[i].decoration = decoration;
decDatas[i].showOnFocus = showOnFocus;
}
/*
* Layout data reserved width and height depend on whether
* there is an image or this is a blank decoration. Always
* set both values since we may be reusing a form data.
*/
if (decoration.getImage() == null) {
formData.width = RESERVED_WIDTH;
formData.height = 0;
} else {
formData.width = SWT.DEFAULT;
formData.height = SWT.DEFAULT;
}
label.setImage(decDatas[i].decoration.getImage());
label.setData(decDatas[i]);
label.setVisible(!showOnFocus);
label.setLayoutData(formData);
}
/**
* Get the control that is decorated by the receiver.
*
* @return the Control decorated by the receiver, or <code>null</code> if
* none has been created yet.
*/
public Control getControl() {
return control;
}
/**
* Get the control that represents the decorated field. This composite
* should be used to lay out the field within its parent.
*
* @return the Control that should be layed out in the field's parent's
* layout. This is typically not the control itself, since
* additional controls are used to represent the decorations.
*/
public Control getLayoutControl() {
return form;
}
/**
* Create the parent composite and a form layout that will be used to manage
* decorations.
*/
private Composite createForm(Composite parent) {
Composite composite = new Composite(parent, SWT.NO_FOCUS);
composite.setLayout(new FormLayout());
return composite;
}
/**
* Add any listeners needed on the target control.
*/
private void addControlListeners() {
control.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent event) {
if (hover != null)
hover.dispose();
}
});
control.addFocusListener(new FocusListener() {
public void focusGained(FocusEvent event) {
controlFocusGained();
}
public void focusLost(FocusEvent event) {
controlFocusLost();
}
});
}
/*
* Return the index in the array of decoration datas that represents the
* specified SWT position.
*
* @param position The SWT constant indicating the position of the
* decoration relative to the field's control. The position should include
* style bits describing both the vertical and horizontal orientation.
* <code>SWT.LEFT</code> and <code>SWT.RIGHT</code> describe the
* horizontal placement of the decoration relative to the field, and the
* constants <code>SWT.TOP</code> and <code>SWT.BOTTOM</code> describe
* the vertical alignment of the decoration relative to the field.
* Decorations always appear on either horizontal side of the field, never
* above or below it. For example, a decoration appearing on the left side
* of the field, at the top, is specified as SWT.LEFT | SWT.TOP. If an image
* decoration already exists in the specified position, it will be replaced
* by the one specified.
*
* @return index the index in the array of decorations that represents the
* specified SWT position. If the position is not an expected position, the
* index representing the top left position will be returned.
*
*/
int indexForPosition(int position) {
switch (position) {
case SWT.LEFT | SWT.BOTTOM:
return LEFT_BOTTOM;
case SWT.RIGHT | SWT.TOP:
return RIGHT_TOP;
case SWT.RIGHT | SWT.BOTTOM:
return RIGHT_BOTTOM;
default:
return LEFT_TOP;
}
}
/**
* Create a form data that will place the decoration at the specified
* position.
*
* @param position
* The SWT constant indicating the position of the decoration
* relative to the field's control. The position should include
* style bits describing both the vertical and horizontal
* orientation. <code>SWT.LEFT</code> and
* <code>SWT.RIGHT</code> describe the horizontal placement of
* the decoration relative to the field, and the constants
* <code>SWT.TOP</code> and <code>SWT.BOTTOM</code> describe
* the vertical alignment of the decoration relative to the
* field. Decorations always appear on either horizontal side of
* the field, never above or below it. For example, a decoration
* appearing on the left side of the field, at the top, is
* specified as SWT.LEFT | SWT.TOP. If an image decoration
* already exists in the specified position, it will be replaced
* by the one specified.
*
*/
private FormData createFormDataForIndex(int index) {
Assert.isTrue(index >= 0 && index < DECORATION_SLOTS,
"Index out of range"); //$NON-NLS-1$
FormData data = new FormData();
switch (index) {
case LEFT_TOP:
data.left = new FormAttachment(0, 0);
data.top = new FormAttachment(0, 0);
return data;
case LEFT_BOTTOM:
data.left = new FormAttachment(0, 0);
data.bottom = new FormAttachment(100, 0);
return data;
case RIGHT_TOP:
data.right = new FormAttachment(100, 0);
data.top = new FormAttachment(0, 0);
return data;
case RIGHT_BOTTOM:
data.right = new FormAttachment(100, 0);
data.bottom = new FormAttachment(100, 0);
return data;
}
// should never get here, making compiler happy
return data;
}
/**
* Show the specified text using the same hover dialog as is used to show
* decorator descriptions. Normally, a decoration's description text will be
* shown in an info hover over the field's control whenever the mouse hovers
* over the decoration. This method can be used to show a decoration's
* description text at other times (such as when the control receives
* focus), or to show other text associated with the field.
*
* <p>
* If there is currently a hover visible, the hover's text will be replaced
* with the specified text.
*
* @param text
* the text to be shown in the info hover, or <code>null</code>
* if no text should be shown.
*/
public void showHoverText(String text) {
showHoverText(text, control);
}
/**
* Hide any hover popups that are currently showing on the control.
* Normally, a decoration's description text will be shown in an info hover
* over the field's control as long as the mouse hovers over the decoration,
* and will be hidden when the mouse exits the control. This method can be
* used to hide a hover that was shown using <code>showHoverText</code>,
* or to programatically hide the current decoration hover.
*
* <p>
* This message has no effect if there is no current hover.
*
*/
public void hideHover() {
if (hover != null) {
hover.setVisible(false);
}
}
/*
* The target control gained focus. Any decorations that should show only
* when they have the focus should be shown here.
*/
private void controlFocusGained() {
for (int i = 0; i < DECORATION_SLOTS; i++) {
if (decDatas[i] != null && decDatas[i].showOnFocus)
setVisible(decDatas[i], true);
}
}
/*
* The target control lost focus. Any decorations that should show only when
* they have the focus should be hidden here.
*/
private void controlFocusLost() {
for (int i = 0; i < DECORATION_SLOTS; i++) {
if (decDatas[i] != null && decDatas[i].showOnFocus)
setVisible(decDatas[i], false);
}
}
/**
* Show the specified decoration. This message has no effect if the
* decoration is already showing, or was not already added to the field
* using <code>addFieldDecoration</code>.
*
* @param decoration
* the decoration to be shown.
*/
public void showDecoration(FieldDecoration decoration) {
FieldDecorationData data = getDecorationData(decoration);
if (data == null)
return;
// record the fact that client would like it to be visible
data.visible = true;
// even if it is supposed to be shown, if the field does not have focus,
// do not show it (yet)
if (!data.showOnFocus || control.isFocusControl())
setVisible(data, true);
}
/**
* Hide the specified decoration. This message has no effect if the
* decoration is already hidden, or was not already added to the field using
* <code>addFieldDecoration</code>.
*
* @param decoration
* the decoration to be hidden.
*/
public void hideDecoration(FieldDecoration decoration) {
FieldDecorationData data = getDecorationData(decoration);
if (data == null)
return;
// Store the desired visibility in the decData. We remember the
// client's instructions so that changes in visibility caused by
// field focus changes won't violate the client's visibility setting.
data.visible = false;
setVisible(data, false);
}
/**
* Update the specified decoration. This message should be used if the image
* or description in the decoration have changed. This message has no
* immediate effect if the decoration is not visible, and no effect at all
* if the decoration was not previously added to the field.
*
* @param decoration
* the decoration to be hidden.
*/
public void updateDecoration(FieldDecoration decoration) {
FieldDecorationData data = getDecorationData(decoration);
if (data == null)
return;
if (data.label != null) {
data.label.setImage(decoration.getImage());
// If the decoration is being shown, and a hover is active,
// update the hover text to display the new description.
if (data.label.getVisible() == true && hover != null) {
showHoverText(decoration.getDescription(), data.label);
}
}
}
/*
* Set the visibility of the specified decoration data. This method does not
* change the visibility value stored in the decData, but instead consults
* it to determine how the visibility should be changed. This method is
* called any time visibility of a decoration might change, whether by
* client API or focus changes.
*/
private void setVisible(FieldDecorationData decData, boolean visible) {
// Check the decData visibility flag, since it contains the client's
// instructions for visibility.
if (visible && decData.visible)
decData.label.setVisible(true);
else
decData.label.setVisible(false);
}
/*
* Get the FieldDecorationData that corresponds to the given decoration.
*/
private FieldDecorationData getDecorationData(FieldDecoration dec) {
for (int i = 0; i < DECORATION_SLOTS; i++) {
if (decDatas[i] != null && dec == decDatas[i].decoration
&& decDatas[i].label != null
&& !decDatas[i].label.isDisposed())
return decDatas[i];
}
return null;
}
/*
* Show the specified text in the hover, positioning the hover near the
* specified control.
*/
private void showHoverText(String text, Control hoverNear) {
if (text == null) {
hideHover();
return;
}
if (hover == null) {
hover = new Hover(hoverNear.getShell());
}
hover.setText(text, hoverNear, control);
hover.setVisible(true);
}
}