/******************************************************************************* * Copyright (c) 2005, 2015 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.util.Util; import org.eclipse.swt.SWT; 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.graphics.GC; import org.eclipse.swt.graphics.Image; 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> * Decorations always appear on either horizontal side of the field, never above * or below it. Decorations can be positioned at the top or bottom of either * side. Future implementations may provide additional positioning options for * decorations. * <p> * By default, DecoratedField will consult the {@link FieldDecorationRegistry} * to determine how much space should be reserved for each decoration. This * allows fields with decorations from different sources to align properly on * the same dialog, since the registry tracks the size of all decorations * registered. Therefore, it is recommended, but not required, that clients of * DecoratedField register the decorations used. In cases where alignment * between different fields is not a concern, clients can use * <code>setUseMaximumDecorationWidth(false)</code> and need not register * their decorations. * <p> * This class is not intended to be subclassed. * * @since 3.2 * @deprecated As of 3.3, clients should use {@link ControlDecoration} instead. */ @Deprecated public class DecoratedField { /** * Cached platform flags for dealing with platform-specific issues. */ private static boolean MAC = Util.isMac(); /** * 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; /** * 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 boolean that indicates whether the maximum decoration width is used * when allocating space for decorations. */ private boolean useMaxDecorationWidth = true; /** * 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 | SWT.TOOL); hoverShell.setBackground(display .getSystemColor(SWT.COLOR_INFO_BACKGROUND)); hoverShell.setForeground(display .getSystemColor(SWT.COLOR_INFO_FOREGROUND)); hoverShell.addPaintListener(pe -> { pe.gc.drawString(text, hm, hm); if (!MAC) { pe.gc.drawPolygon(getPolygon(true)); } }); hoverShell.addMouseListener(new MouseAdapter() { @Override 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(); form.setTabList(new Control[] { control }); // Set up the initial layout data. FormData data = new FormData(); data.left = new FormAttachment(0, 0); data.top = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); data.bottom = new FormAttachment(100, 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, decoration.getImage()); label = new Label(form, SWT.HORIZONTAL | SWT.VERTICAL | SWT.CENTER); label.addMouseTrackListener(new MouseTrackListener() { @Override public void mouseHover(MouseEvent event) { FieldDecorationData decData = (FieldDecorationData) event.widget .getData(); String desc = decData.decoration.getDescription(); if (desc != null) { showHoverText(desc, label); } } @Override public void mouseEnter(MouseEvent event) { } @Override 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; } label.setImage(decDatas[i].decoration.getImage()); label.setData(decDatas[i]); label.setLayoutData(formData); label.setVisible(!showOnFocus); // Since sizes may have changed or there could be a new position // defined, we need to update layout data on the control. updateControlAttachments(i, decDatas[i]); } /* * A decoration at the specified index has been added. Update the control's * attachments if it has not previously been attached on that side or if it * was attached to a decoration with a lesser width. */ private void updateControlAttachments(int index, FieldDecorationData decData) { FormData formData = (FormData) control.getLayoutData(); int newWidth = widthOf(decData.decoration.getImage()); // opposing represents the location of the decoration above or below // the one in question. int opposing; switch (index) { case LEFT_TOP: case LEFT_BOTTOM: if (index == LEFT_TOP) { opposing = LEFT_BOTTOM; } else { opposing = LEFT_TOP; } if (decDatas[opposing] == null) { // No decorator on the opposing side. // Attach the control to this decorator formData.left = new FormAttachment(decData.label); } else if (decDatas[opposing].data.width < newWidth) { // Decorator on opposing side is the smaller one. Attach // control to the new one. formData.left = new FormAttachment(decData.label); // Center align the smaller one relative to the larger one. decDatas[opposing].data.left.alignment = SWT.CENTER; decDatas[opposing].data.left.control = decData.label; } else { // The new decorator is the smaller one. Keep the // control attached to the opposing one. formData = null; // Horizontally center the smaller one relative to the larger // one. decData.data.left.alignment = SWT.CENTER; decData.data.left.control = decDatas[opposing].label; } break; /* * The only real difference in right side cases is that we are attaching * the right side of the control to the wider decoration rather than the * left side of the control. Other concerns (horizontally aligning the * smaller decoration relative to the larger one) are the same. */ case RIGHT_TOP: case RIGHT_BOTTOM: if (index == RIGHT_TOP) { opposing = RIGHT_BOTTOM; } else { opposing = RIGHT_TOP; } if (decDatas[opposing] == null) { // No decorator on the opposing side. // Attach the control to this decorator. formData.right = new FormAttachment(decData.label); } else if (decDatas[opposing].data.width < newWidth) { // Decorator on opposing side is the smaller one. Attach // control to the new one. formData.right = new FormAttachment(decData.label); // Center align the smaller one to the larger one. // Note that this could be done using the left or right // attachment, we use the right since it is already // created for all right-side decorations. decDatas[opposing].data.right.alignment = SWT.CENTER; decDatas[opposing].data.right.control = decData.label; } else { // The new decorator is the smaller one. Keep the // control attached to the opposing one. formData = null; // Horizontally center align the smaller one to the // larger one. decData.data.right.alignment = SWT.CENTER; decData.data.right.control = decDatas[opposing].label; } break; default: return; } if (formData != null) { // Form data was updated. control.setLayoutData(formData); form.layout(); } } /** * 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); // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=126553 composite.setBackgroundMode(SWT.INHERIT_DEFAULT); composite.setLayout(new FormLayout()); return composite; } /** * Add any listeners needed on the target control. */ private void addControlListeners() { control.addDisposeListener(event -> { if (hover != null) { hover.dispose(); } }); control.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent event) { controlFocusGained(); } @Override 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. * * @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. * */ private 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 index the index in the decDatas describing the position of the * decoration. * * @param image the image shown in the decoration. * */ private FormData createFormDataForIndex(int index, Image image) { 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); break; case LEFT_BOTTOM: data.left = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); break; case RIGHT_TOP: data.right = new FormAttachment(100, 0); data.top = new FormAttachment(0, 0); break; case RIGHT_BOTTOM: data.right = new FormAttachment(100, 0); data.bottom = new FormAttachment(100, 0); break; } data.width = widthOf(image); data.height = SWT.DEFAULT; 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); } /** * Set a boolean that indicates whether the receiver should use the * decoration registry's maximum decoration width when allocating space for * decorations. The default value is <code>true</code>. Using the maximum * decoration width is useful so that decorated fields on the same dialog * that have different decoration widths will all align. This also allows * client dialogs to align non-decorated fields with decorated fields by * consulting the maximum decoration width. * </p> * <p> * Clients may wish to set this value to <code>false</code> in cases where * space usage is more important than alignment of fields. This value must * be set before the decorations are added in order to ensure proper * alignment. * </p> * * @param useMaximumWidth * <code>true</code> if the maximum decoration width should be * used as the size for all decorations, <code>false</code> if * only the decoration size should be used. * * @see FieldDecorationRegistry#getMaximumDecorationWidth() */ public void setUseMaximumDecorationWidth(boolean useMaximumWidth) { useMaxDecorationWidth = useMaximumWidth; } /* * Return the width appropriate for the specified decoration image. */ private int widthOf(Image image) { if (image == null) { return 0; } return useMaxDecorationWidth ? FieldDecorationRegistry.getDefault() .getMaximumDecorationWidth() : image.getBounds().width; } }