/******************************************************************************* * Copyright (c) 2000, 2010 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 (CCombo) * compeople AG - adjustments for autocompletion *******************************************************************************/ package org.eclipse.riena.ui.swt; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import org.eclipse.core.runtime.Assert; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.VerifyListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Button; 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.Layout; import org.eclipse.swt.widgets.List; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.TypedListener; import org.eclipse.swt.widgets.Widget; import org.eclipse.riena.ui.swt.facades.ClipboardFacade; import org.eclipse.riena.ui.swt.facades.SWTFacade; import org.eclipse.riena.ui.swt.utils.SwtUtilities; /** * The CompletionCombo class represents a selectable user interface object that combines a text field and a list and issues notification when an item is * selected from the list. The list will automatically pop-up when the text control is focused and the user is typing. * <p> * This class is abstract. There are several implementations along these axes: RCP-specific or RAP-specific, and with images for each item (using a Table) or * without images for each item (using a List). This yields the following combinations: * <ul> * <li>ComplectionComboRCP / CompletionComboRAP – a CompletionCombo with a text field and a list control</li> * <li>ComplectionComboWithImageRCP / CompletionComboWithImageRAP – a CompletionCombo with a text field, with an optional image on the left, and a table * control (which can show an image next to each item)</li> * </ul> * <p> * <b>Important:</b> use {@code UIControlsFactory.createCompletionCombo(...)} and {@code UIControlsFactory.createCompletionComboWithImage(...)} to automatically * get the correct RCP- or RAP-specific instance. * <p> * CompletionCombo was written to work around certain limitations in the native combo box. There is no is no strict requirement that CompletionCombo look or * behave the same as the native combo box. * <p> * Note that although this class is a subclass of <code>Composite</code>, it does not make sense to add children to it, or set a layout on it. * <dl> * <dt><b>Styles:</b> * <dd>BORDER, READ_ONLY, FLAT</dd> * <dt><b>Events:</b> * <dd>DefaultSelection, Modify, Selection, Verify</dd> * </dl> * * @since 2.0 */ public abstract class CompletionCombo extends Composite { /** * Implementations of this interface can be added to the {@link CompletionCombo} to get notified when the combo popup is hidden. * <p> * IMPORTANT: This interface is not API * * @since 5.0 */ public interface DropDownListener { void hidden(); } private static final SWTFacade SWT_FACADE = SWTFacade.getDefault(); /** * Stores all allowed input characters that are not letters or digits. This data is computed as items are added to the CompletionCombo and used in * {@link #isInputChar(char)}. */ private final Set<Character> inputChars = new HashSet<Character>(); /** * Label for showing an image next to the combo's text control. This control is optional and may be null. */ private Label label; /** * The text control for this combo. */ private Text text; /** * The list control for this combo. Can be a {@link List} or {@link Table}. */ private Control list; /** * Pop-up window (Shell) for the list control */ private Shell popup; /** * Button to trigger showing the selection pop-up. */ private Button arrow; private Listener listener, filter; private Color listForeground, listBackground; private Font font; private Shell _shell; private int visibleItemCount = 5; private boolean hasFocus; /** * Stores the enablement setting for this widget, independelty if of parent enablement (v.s. isEnabled() which depends on parent enablement) */ private boolean isEnabled = true; /** * Fix for 335129: ignore FocusOut event when we move the focus from the Text widget to the list popup. */ private boolean ignoreFocusOut; private AutoCompletionMode autoCompletionMode; /** * Algorithm for flashing the combo's background when non-matching input is rejected. */ private IFlashDelegate flashDelegate; private Menu systemContextMenu; private Menu emptyContextMenu; private final ArrayList<DropDownListener> dropDownListeners = new ArrayList<CompletionCombo.DropDownListener>(); /** * This enumeration is used to configure the the way the autocompletion works. */ public enum AutoCompletionMode { /** * The Combo accepts all typed words and and just stops tracking the list items if no match is found. */ ALLOW_MISSMATCH, /** * The Combo rejects typed characters that would make the String in the textfield not match any of the items in the list. */ NO_MISSMATCH, /** * The Combo selects the items beginning with the character that was just typed (ignoring case). If no match is found the input is ignored. When * reaching the end of the list of matches, the selection wraps around and continues from the beginning. * <p> * Examples: * <ul> * <li>'a' selects the 1st item beginning with 'a',</li> * <li>'aa' selects the 2nd item beginning with 'a',</li> * <li>'aaa' the 3rd item,</li> * <li>assumming there are only two items beginning with 'a', then 'aaa' would wrap and select the 1st item,</li> * <li>'ad' selects the 1st item beginning with 'd'</li> * </ul> * * @since 3.0 */ FIRST_LETTER_MATCH } static int checkStyle(final int style) { final int mask = SWT.BORDER | SWT.READ_ONLY | SWT.FLAT | SWT.LEFT_TO_RIGHT | SWTFacade.RIGHT_TO_LEFT; return SWT.NO_FOCUS | (style & mask); } /** * Constructs a new instance of this class given its parent and a style value describing its behavior and appearance. * <p> * The style value is either one of the style constants defined in class <code>SWT</code> which is applicable to instances of this class, or must be built * by <em>bitwise OR</em>'ing together (that is, using the <code>int</code> "|" operator) two or more of those <code>SWT</code> style constants. The class * description lists the style constants that are applicable to the class. Style bits are also inherited from superclasses. * * @param parent * a control which will be the parent of the new instance (cannot be null) * @param style * the style of control to construct * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the parent is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li> * </ul> * * @see SWT#BORDER * @see SWT#READ_ONLY * @see SWT#FLAT * @see Widget#getStyle() */ protected CompletionCombo(final Composite parent, int style) { super(parent, style = checkStyle(style)); _shell = super.getShell(); int textStyle = SWT.SINGLE; if ((style & SWT.READ_ONLY) != 0) { textStyle |= SWT.READ_ONLY; } if ((style & SWT.FLAT) != 0) { textStyle |= SWT.FLAT; } label = createLabel(this); text = new Text(this, textStyle); text.setBackground(getBackground()); systemContextMenu = text.getMenu(); emptyContextMenu = new Menu(text); int arrowStyle = SWT.ARROW | SWT.DOWN; if ((style & SWT.FLAT) != 0) { arrowStyle |= SWT.FLAT; } arrow = new Button(this, arrowStyle); listener = new Listener() { public void handleEvent(final Event event) { if (isDisposed()) { return; } if (popup == event.widget) { popupEvent(event); return; } if (text == event.widget) { textEvent(event); return; } if (list == event.widget) { listEvent(event); return; } if (arrow == event.widget) { arrowEvent(event); return; } if (CompletionCombo.this == event.widget) { comboEvent(event); return; } if (getShell() == event.widget) { // 335128: fixed by using syncExec instead of asyncExec getDisplay().syncExec(new Runnable() { public void run() { if (isDisposed()) { return; } if (event.type == SWT.Deactivate) { closeDropDownAfterDeactivatingShell(); } handleFocus(SWT.FocusOut); } }); } } }; filter = new Listener() { public void handleEvent(final Event event) { if (isDisposed()) { return; } final Shell shell = ((Control) event.widget).getShell(); if (shell == CompletionCombo.this.getShell()) { if (event.type == SWT.MouseDown && !isClickedInCombo()) { dropDown(false); defaultTextSelection(); } else { if (!ignoreFocusOut) { handleFocus(SWT.FocusOut); } } } } private boolean isClickedInCombo() { final Point point = toControl(getDisplay().getCursorLocation()); final Point size = getSize(); final Rectangle rect = new Rectangle(0, 0, size.x, size.y); return rect.contains(point); } }; final int[] comboEvents = { SWT.Dispose, SWT.FocusIn, SWT.FocusOut, SWT.Move, SWT.Resize }; for (final int comboEvent : comboEvents) { this.addListener(comboEvent, listener); } final int[] textEvents = { SWT.DefaultSelection, SWT.DragDetect, SWT.KeyDown, SWT.KeyUp, SWT.MenuDetect, SWT.Modify, SWT.MouseDown, SWT.MouseUp, SWT.MouseDoubleClick, SWTFacade.MouseEnter, SWTFacade.MouseExit, SWTFacade.MouseHover, SWTFacade.MouseMove, SWTFacade.MouseWheel, SWT.Traverse, SWT.FocusIn, SWT.Verify }; for (final int textEvent : textEvents) { text.addListener(textEvent, listener); } final int[] arrowEvents = { SWT.DragDetect, SWT.MouseDown, SWTFacade.MouseEnter, SWTFacade.MouseExit, SWTFacade.MouseHover, SWTFacade.MouseMove, SWT.MouseUp, SWTFacade.MouseWheel, SWT.Selection, SWT.FocusIn }; for (final int arrowEvent : arrowEvents) { arrow.addListener(arrowEvent, listener); } createPopup(null, null, -1); setAutoCompletionMode(AutoCompletionMode.NO_MISSMATCH); } /** * Create an optional label for showing the image associated with contents of the combo's text control. The label will be placed to the right of the combo's * text control. * * @return a Label or null to not show any images. * @since 3.0 */ protected abstract Label createLabel(final Composite parent); /** * Create a {@link Control} for selecting entries in this combo. * * @return a control instance; never null. Typically this is a {@link List} or {@link Table} control. * @since 3.0 */ protected abstract Control createList(final Composite parent); /** * Clears all selected elements from the list control. * * @param list * the list control; never null * @since 3.0 */ protected abstract void deselectAll(Control list); /** * Returns the image associated with the requested item in the selection control. * * @param list * the list control; never null * @param index * the zero-relative index of the item * @return an Image or null. The later case will occur when the selection control does not associate images with it's items (example List) or the individual * item has no image (example Table). * @throws IllegalArgumentException * if index is out of range * @since 3.0 */ protected abstract Image getImage(Control list, int index); /** * Return the String associated with the requested item in the selection control. * * @param list * the list control; never null * @param index * the zero-relative index of the item * @return a String; never null * @throws IllegalArgumentException * if index is out of range * @since 3.0 */ protected abstract String getItem(Control list, int index); /** * Returns the height of each item in the list control. * <p> * Implementation notes: this assumes that all items have the same height. The item height is used to calculate the size of the selection pop-up. * * @param list * the list control; never null * @return the height of each item in pixels * * @since 3.0 */ protected abstract int getItemHeight(Control list); /** * Returns the Images for the items in the list control. The order of the Images in the array must correspond to the order of the items in the control. * * @param list * the list control; never null * @return an array of Images; may be empty. May be null if the list control does not support images. Individual entries may be null if the corresponding * item does not have an image. * @since 3.0 */ protected abstract Image[] getImages(Control list); /** * Returns the Strings for the items in the list control. The order of the Strings in the array must correspond to the order of items in the control. * * @param list * the list control; never null * @return an array of Strings; may be empty; never null. Individual entries may be empty but never null. * @since 3.0 */ protected abstract String[] getItems(Control list); /** * Returns the number of items in the list control. * * @param list * the list control; never null * @return the number of items in the list control * @since 3.0 */ protected abstract int getItemCount(Control list); /** * Returns the zero-relative index of the item which is currently selected in the list control. Returns -1 if no item is selected. * * @param list * the list control; never null * @return the zero-relative index of the currently selected item; -1 if no item is selected * @since 3.0 */ protected abstract int getSelectionIndex(Control list); /** * Returns the zero-relative index of the item currently shown in the top row of the list control. The index changes as the control is scrolled or items are * added / removed. * * @param list * the list control; never null * @return the zero-relative index of the item currently shown in the top row of the control * @since 3.0 */ protected abstract int getTopIndex(Control list); /** * Searches from the given {@code start} position and an item matching the given {@code string} is found and returns the zero-relative index of the match. * Returns -1 if no matching item is found. * * @param list * the list control; never null * @return the zero-relative index of the matched item; -1 if no match was found * @since 3.0 */ protected abstract int indexOf(Control list, String string, int start); /** * Removes all items from the list control. * * @param list * the list control; never null * @since 3.0 */ protected abstract void removeAll(final Control list); /** * Creates the given items in the list control. The list control is cleared at the beginning of this operation * * @param list * the list control; never null * @param items * an array of Strings for the items in the control; never null. Individual entries cannot be null. * @param images * an array of Images for the items in the control. May be null if no images should be used. Individual entries may be null, if no image should * be used for that item. * @throws RuntimeException * if the images and items arrays have different lengths * @since 3.0 */ protected abstract void setItems(Control list, String[] items, Image[] images); /** * Selects the item at the given zero-relative {@code index} in the list control. * * @param list * the list control; never null * @param index * the zero-relative index of the item to select. Values that are out of range are ignored. * @since 3.0 */ protected abstract void setSelection(Control list, int index); /** * Scrolls the contents of the list control, so that the item at the given zero-relative {@code index} is at the top of the control. * * @param list * the list control; never null * @param index * the zero-relative index of the item to show at the top of the control * @since 3.0 */ protected abstract void setTopIndex(Control list, int index); /** * TODO comment * * @since 6.1 */ protected abstract void updateExtendedText(final Control list, final int index); /** * Returns the arrow-button control for this combo. This is the arrow-down button at the right side of the combo. * * @return a Button control; never null * @since 3.0 */ protected Button getButtonControl() { return arrow; } /** * Returns the list control for this combo. Can be a {@link List} or {@link Table} widget. * * @return the list control; never null * @since 3.0 */ protected Control getListControl() { return list; } /** * Returns the text control for this combo. * * @return a Text control; never null * @since 3.0 */ protected Text getTextControl() { return text; } /** * Adds the listener to the collection of listeners who will be notified when the receiver's text is modified, by sending it one of the messages defined in * the <code>ModifyListener</code> interface. * * @param listener * the listener which should be notified * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * * @see ModifyListener * @see #removeModifyListener */ public void addModifyListener(final ModifyListener listener) { checkWidget(); if (listener == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } final TypedListener typedListener = new TypedListener(listener); addListener(SWT.Modify, typedListener); } /** * Adds the listener to the collection of listeners who will be notified when the user changes the receiver's selection, by sending it one of the messages * defined in the <code>SelectionListener</code> interface. * <p> * <code>widgetSelected</code> is called when the combo's list selection changes. <code>widgetDefaultSelected</code> is typically called when ENTER is * pressed the combo's text area. * </p> * * @param listener * the listener which should be notified when the user changes the receiver's selection * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * * @see SelectionListener * @see #removeSelectionListener * @see SelectionEvent */ public void addSelectionListener(final SelectionListener listener) { checkWidget(); if (listener == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } final TypedListener typedListener = new TypedListener(listener); addListener(SWT.Selection, typedListener); addListener(SWT.DefaultSelection, typedListener); } /** * Adds the listener to the collection of listeners who will be notified when the receiver's text is verified, by sending it one of the messages defined in * the <code>VerifyListener</code> interface. * * @param listener * the listener which should be notified * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * * @see VerifyListener * @see #removeVerifyListener */ public void addVerifyListener(final VerifyListener listener) { checkWidget(); if (listener == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } final TypedListener typedListener = new TypedListener(listener); addListener(SWT.Verify, typedListener); } /** * Adds the given {@link DropDownListener} to the listeners list. * <p> * IMPORTANT: This method is not API * * @since 5.0 */ public void addDropDownListener(final DropDownListener listener) { if (!dropDownListeners.contains(dropDownListeners)) { dropDownListeners.add(listener); } } void arrowEvent(final Event event) { switch (event.type) { case SWT.FocusIn: handleFocus(SWT.FocusIn); break; case SWT.DragDetect: case SWT.MouseDown: case SWT.MouseUp: case SWTFacade.MouseMove: case SWTFacade.MouseEnter: case SWTFacade.MouseExit: case SWTFacade.MouseHover: Point pt = getDisplay().map(arrow, this, event.x, event.y); event.x = pt.x; event.y = pt.y; notifyListeners(event.type, event); event.type = SWT.None; break; case SWTFacade.MouseWheel: pt = getDisplay().map(arrow, this, event.x, event.y); event.x = pt.x; event.y = pt.y; notifyListeners(SWTFacade.MouseWheel, event); event.type = SWT.None; if (isDisposed()) { break; } if (!event.doit) { break; } if (event.count != 0) { event.doit = false; final int oldIndex = getSelectionIndex(); if (event.count > 0) { select(Math.max(oldIndex - 1, 0)); } else { select(Math.min(oldIndex + 1, getItemCount() - 1)); } if (oldIndex != getSelectionIndex()) { final Event e = new Event(); e.time = event.time; e.stateMask = event.stateMask; notifyListeners(SWT.Selection, e); } if (isDisposed()) { break; } } break; case SWT.Selection: dropDown(!isDropped()); setFocus(); break; default: break; } } /** * Sets the selection in the receiver's text field to an empty selection starting just before the first character. If the text field is editable, this has * the effect of placing the i-beam at the start of the text. * <p> * Note: To clear the selected items in the receiver's list, use <code>deselectAll()</code>. * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that * created the receiver</li> * </ul> * * @see #deselectAll */ public void clearSelection() { checkWidget(); text.clearSelection(); } void comboEvent(final Event event) { switch (event.type) { case SWT.Dispose: removeListener(SWT.Dispose, listener); notifyListeners(SWT.Dispose, event); event.type = SWT.None; if (popup != null && !popup.isDisposed()) { list.removeListener(SWT.Dispose, listener); popup.dispose(); } final Shell shell = getShell(); shell.removeListener(SWT.Deactivate, listener); final Display display = getDisplay(); display.removeFilter(SWT.FocusIn, filter); popup = null; text = null; list = null; arrow = null; _shell = null; break; case SWT.FocusIn: final Control focusControl = getDisplay().getFocusControl(); if (focusControl == arrow || focusControl == list) { return; } setFocus(); break; case SWT.FocusOut: if (autoCompletionMode == AutoCompletionMode.FIRST_LETTER_MATCH) { text.setSelection(0, 0); } if (isDropped()) { dropDown(false); } break; case SWT.Move: dropDown(false); break; case SWT.Resize: internalLayout(false); break; default: break; } } @Override public Point computeSize(final int wHint, final int hHint, final boolean changed) { checkWidget(); int width = 0, height = 0; final String[] items = getItems(list); final GC gc = new GC(text); final int spacer = gc.stringExtent(" ").x; //$NON-NLS-1$ int textWidth = gc.stringExtent(text.getText()).x; for (final String item : items) { textWidth = Math.max(gc.stringExtent(item).x, textWidth); } gc.dispose(); final Point labelSize = label != null ? label.computeSize(SWT.DEFAULT, SWT.DEFAULT) : new Point(0, 0); final Point textSize = text.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed); final Point arrowSize = arrow.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed); final Point listSize = list.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed); final int borderWidth = getBorderWidth(); height = Math.max(textSize.y, arrowSize.y); height = Math.max(height, labelSize.y); width = Math.max(textWidth + 2 * spacer + arrowSize.x + 2 * borderWidth, listSize.x); if (label != null) { width += labelSize.x + (2 * spacer); } if (wHint != SWT.DEFAULT) { width = wHint; } if (hHint != SWT.DEFAULT) { height = hHint; } return new Point(width + 2 * borderWidth, height + 2 * borderWidth); } void createPopup(final String[] items, final Image[] images, final int selectionIndex) { // create shell and list popup = new Shell(getShell(), SWT.NO_TRIM | SWT.ON_TOP); list = createList(popup); if (font != null) { list.setFont(font); } if (listForeground != null) { list.setForeground(listForeground); } if (listBackground != null) { list.setBackground(listBackground); } final int[] popupEvents = { SWT.Close, SWTFacade.Paint, SWT.Deactivate }; for (final int popupEvent : popupEvents) { popup.addListener(popupEvent, listener); } final int[] listEvents = { SWT.MouseUp, SWT.Selection, SWT.Traverse, SWT.KeyDown, SWT.KeyUp, SWT.FocusIn, SWTFacade.MouseWheel, SWT.Dispose }; for (final int listEvent : listEvents) { list.addListener(listEvent, listener); } if (items != null) { setItems(list, items, images); } if (selectionIndex != -1) { setSelection(list, selectionIndex); } } /** * Deselects the item at the given zero-relative index in the receiver's list. If the item at the index was already deselected, it remains deselected. * Indices that are out of range are ignored. * * @param index * the index of the item to deselect * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void deselect(final int index) { checkWidget(); if (0 <= index && index < getItemCount(list) && index == getSelectionIndex(list) && text.getText().equals(getItem(list, index))) { clearImage(); text.setText(""); //$NON-NLS-1$ updateExtendedText(list, -1); deselectAll(list); } } /** * Deselects all selected items in the receiver's list. * <p> * Note: To clear the selection in the receiver's text field, use <code>clearSelection()</code>. * </p> * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * * @see #clearSelection */ public void deselectAll() { checkWidget(); clearImage(); text.setText(""); //$NON-NLS-1$ updateExtendedText(list, -1); deselectAll(list); } void dropDown(final boolean drop) { if (drop == isDropped()) { return; } if (!drop) { popup.setVisible(false); if (!isDisposed() && isFocusControl()) { setFocus(); } notifyDropDownListeners(); return; } if (!isVisible()) { return; } if (getShell() != popup.getParent()) { final String[] items = getItems(list); final Image[] images = getImages(list); final int selectionIndex = getSelectionIndex(list); list.removeListener(SWT.Dispose, listener); popup.dispose(); popup = null; list = null; createPopup(items, images, selectionIndex); } final Point listSize = computeListSize(list, visibleItemCount); list.setBounds(1, 1, listSize.x, listSize.y); // always select the first element, if selection is empty and [ // autocompletion mode is FIRST_LETTER_MATCH // if (autoCompletionMode == AutoCompletionMode.FIRST_LETTER_MATCH && getItemCount() > 0 // && getSelectionIndex() == -1) { // setMatchingTextAndSelection(0, 0, getItem(0)); // } final int index = getSelectionIndex(list); if (index != -1) { setTopIndex(list, index); } popup.setBounds(computePopupBounds()); popup.setVisible(true); if (isFocusControl()) { setFocus(); } onAfterDropDown(list); } private Rectangle computePopupBounds() { final Display display = getDisplay(); final Rectangle listRect = list.getBounds(); final Rectangle parentRect = display.map(getParent(), null, getBounds()); final Point comboSize = getSize(); final Rectangle displayRect = getMonitor().getClientArea(); final int width = Math.max(comboSize.x, listRect.width + 2); final int height = listRect.height + 2; int x = parentRect.x; int y = parentRect.y + comboSize.y; if (y + height > displayRect.y + displayRect.height) { y = parentRect.y - height; } if (x + width > displayRect.x + displayRect.width) { x = displayRect.x + displayRect.width - listRect.width; } return new Rectangle(x, y, width, height); } /** * @since 6.0 */ protected Point computeListSize(final Control list, final int visibleItemCount) { final Point size = getSize(); int itemCount = getItemCount(list); itemCount = (itemCount == 0) ? visibleItemCount : Math.min(visibleItemCount, itemCount); final int itemHeight = getItemHeight(list) * itemCount; final Point listSize = list.computeSize(SWT.DEFAULT, itemHeight, false); return new Point(Math.max(size.x - 2, listSize.x), listSize.y); } /** * @since 6.0 */ protected void onAfterDropDown(final Control list) { } private void notifyDropDownListeners() { for (final DropDownListener l : new ArrayList<DropDownListener>(dropDownListeners)) { l.hidden(); } } /** * Returns the background color of the combo's List control. * * @return a Color instance * @since 3.0 */ public Color getListBackground() { return listBackground != null ? listBackground : getBackground(); } /** * Returns the background color of the combo's Text control. * * @return a Color instance * @since 3.0 */ public Color getTextBackground() { return text.getBackground(); } /** * Gets the editable state. * * @return true if the receiver is editable, false otherwise * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public boolean getEditable() { checkWidget(); return text.getEditable(); } /** * Returns the item at the given, zero-relative index in the receiver's list. Throws an exception if the index is out of range. * * @param index * the index of the item to return * @return the item at the given index * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_RANGE - if the index is not between 0 and the number of elements in the list minus 1 (inclusive)</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public String getItem(final int index) { checkWidget(); return getItem(list, index); } /** * Returns the number of items contained in the receiver's list. * * @return the number of items * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int getItemCount() { checkWidget(); return getItemCount(list); } /** * Returns the height of the area which would be used to display <em>one</em> of the items in the receiver's list. * * @return the height of one item * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int getItemHeight() { checkWidget(); return getItemHeight(list); } /** * Returns an array of <code>String</code>s which are the items in the receiver's list. * <p> * Note: This is not the actual structure used by the receiver to maintain its list of items, so modifying the array will not affect the receiver. * </p> * * @return the items in the receiver's list * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public String[] getItems() { checkWidget(); return getItems(list); } /** * Returns <code>true</code> if the receiver's list is visible, and <code>false</code> otherwise. * <p> * If one of the receiver's ancestors is not visible or some other condition makes the receiver not visible, this method may still indicate that it is * considered visible even though it may not actually be showing. * </p> * * @return the receiver's list's visibility state * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public boolean getListVisible() { checkWidget(); return isDropped(); } @Override public Menu getMenu() { return text.getMenu(); } /** * Returns a <code>Point</code> whose x coordinate is the start of the selection in the receiver's text field, and whose y coordinate is the end of the * selection. The returned values are zero-relative. An "empty" selection as indicated by the the x and y coordinates having the same value. * * @return a point representing the selection start and end * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public Point getSelection() { checkWidget(); return text.getSelection(); } /** * Returns the zero-relative index of the item which is currently selected in the receiver's list, or -1 if no item is selected. * * @return the index of the selected item * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int getSelectionIndex() { checkWidget(); return getSelectionIndex(list); } @Override public Shell getShell() { checkWidget(); final Shell shell = super.getShell(); if (shell != _shell) { if (_shell != null && !_shell.isDisposed()) { _shell.removeListener(SWT.Deactivate, listener); } _shell = shell; } return _shell; } @Override public int getStyle() { int style = super.getStyle(); style &= ~SWT.READ_ONLY; if (!text.getEditable()) { style |= SWT.READ_ONLY; } return style; } /** * Returns a string containing a copy of the contents of the receiver's text field. * * @return the receiver's text * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public String getText() { checkWidget(); return text.getText(); } /** * Returns the height of the receivers's text field. * * @return the text height * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int getTextHeight() { checkWidget(); return text.getLineHeight(); } /** * Returns the maximum number of characters that the receiver's text field is capable of holding. If this has not been changed by * <code>setTextLimit()</code>, it will be the constant <code>Combo.LIMIT</code>. * * @return the text limit * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int getTextLimit() { checkWidget(); return text.getTextLimit(); } /** * Gets the number of items that are visible in the drop down portion of the receiver's list. * * @return the number of items that are visible * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int getVisibleItemCount() { checkWidget(); return visibleItemCount; } void handleFocus(final int type) { switch (type) { case SWT.FocusIn: if (hasFocus) { return; } defaultTextSelection(); hasFocus = true; Shell shell = getShell(); shell.removeListener(SWT.Deactivate, listener); shell.addListener(SWT.Deactivate, listener); Display display = getDisplay(); display.removeFilter(SWT.FocusIn, filter); display.addFilter(SWT.FocusIn, filter); display.removeFilter(SWT.MouseDown, filter); display.addFilter(SWT.MouseDown, filter); Event e = new Event(); notifyListeners(SWT.FocusIn, e); break; case SWT.FocusOut: if (!hasFocus) { return; } final Control focusControl = getDisplay().getFocusControl(); if (focusControl == arrow || focusControl == list || focusControl == text) { return; } hasFocus = false; shell = getShell(); shell.removeListener(SWT.Deactivate, listener); display = getDisplay(); display.removeFilter(SWT.FocusIn, filter); display.removeFilter(SWT.MouseDown, filter); e = new Event(); notifyListeners(SWT.FocusOut, e); break; default: break; } } /** * Searches the receiver's list starting at the first item (index 0) until an item is found that is equal to the argument, and returns the index of that * item. If no item is found, returns -1. * * @param string * the search item * @return the index of the item * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the string is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int indexOf(final String string) { checkWidget(); if (string == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } return indexOf(list, string, 0); } /** * Searches the receiver's list starting at the given, zero-relative index until an item is found that is equal to the argument, and returns the index of * that item. If no item is found or the starting index is out of range, returns -1. * * @param string * the search item * @param start * the zero-relative index at which to begin the search * @return the index of the item * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the string is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public int indexOf(final String string, final int start) { checkWidget(); if (string == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } return indexOf(list, string, start); } /** * Returns true if the item selection pop-up is dropped (=open), false otherwise * * @return true if the selection pop-up is dropped, false otherwise * @since 3.0 */ protected boolean isDropped() { return !SwtUtilities.isDisposed(popup) && popup.getVisible(); } @Override public boolean isFocusControl() { checkWidget(); if (text.isFocusControl() || arrow.isFocusControl() || list.isFocusControl() || popup.isFocusControl()) { return true; } return super.isFocusControl(); } /** * @since 6.1 */ protected void internalLayout(final boolean changed) { if (isDropped()) { dropDown(false); } final Rectangle rect = getClientArea(); final int width = rect.width; final int height = rect.height; final Point arrowSize = arrow.computeSize(SWT.DEFAULT, height, changed); if (label != null) { final Point labelSize = arrow.computeSize(16, height, changed); labelSize.x += 3; label.setBounds(3, 0, labelSize.x - 3, height); text.setBounds(labelSize.x, 0, width - arrowSize.x - labelSize.x, height); arrow.setBounds(width - arrowSize.x, 0, arrowSize.x, arrowSize.y); } else { text.setBounds(0, 0, width - arrowSize.x, height); arrow.setBounds(width - arrowSize.x, 0, arrowSize.x, arrowSize.y); } } void listEvent(final Event event) { switch (event.type) { case SWT.Dispose: if (getShell() != popup.getParent()) { final String[] items = getItems(list); final Image[] images = getImages(list); final int selectionIndex = getSelectionIndex(list); popup = null; list = null; createPopup(items, images, selectionIndex); } break; case SWT.FocusIn: handleFocus(SWT.FocusIn); break; case SWT.MouseUp: if (event.button != 1) { return; } dropDown(false); break; case SWT.Selection: if (SwtUtilities.isDisposed(text) || SwtUtilities.isDisposed(list)) { return; } final int index = getSelectionIndex(list); if (index == -1) { return; } setImage(index); try { text.setText(getItem(list, index)); updateExtendedText(list, index); } catch (final NullPointerException ex) { // At this point the widget may have been disposed. // If so, do not continue. if (isDisposed()) { return; } throw ex; } defaultTextSelection(); //setSelection(list, index); Event e = new Event(); e.time = event.time; e.stateMask = event.stateMask; e.doit = event.doit; notifyListeners(SWT.Selection, e); event.doit = e.doit; break; case SWT.Traverse: switch (event.detail) { case SWT.TRAVERSE_RETURN: case SWT.TRAVERSE_ESCAPE: case SWTFacade.TRAVERSE_ARROW_PREVIOUS: case SWTFacade.TRAVERSE_ARROW_NEXT: event.doit = false; break; case SWT.TRAVERSE_TAB_NEXT: case SWT.TRAVERSE_TAB_PREVIOUS: event.doit = SWT_FACADE.traverse(text, event.detail); event.detail = SWT.TRAVERSE_NONE; if (event.doit) { dropDown(false); } return; default: break; } e = new Event(); e.time = event.time; e.detail = event.detail; e.doit = event.doit; e.character = event.character; e.keyCode = event.keyCode; SWT_FACADE.copyEventKeyLocation(event, e); notifyListeners(SWT.Traverse, e); event.doit = e.doit; event.detail = e.detail; break; case SWT.KeyUp: e = new Event(); e.time = event.time; e.character = event.character; e.keyCode = event.keyCode; SWT_FACADE.copyEventKeyLocation(event, e); e.stateMask = event.stateMask; notifyListeners(SWT.KeyUp, e); event.doit = e.doit; break; case SWT.KeyDown: if (event.character == SWT.ESC) { // Escape key cancels popup list dropDown(false); } if ((event.stateMask & SWT.ALT) != 0 && (event.keyCode == SWT.ARROW_UP || event.keyCode == SWT.ARROW_DOWN)) { dropDown(false); } if ((event.character == SWT.DEL || event.character == SWT.BS) && autoCompletionMode == AutoCompletionMode.FIRST_LETTER_MATCH) { clearImage(); if (SwtUtilities.isDisposed(text) || SwtUtilities.isDisposed(list)) { return; } text.setText(""); //$NON-NLS-1$ updateExtendedText(list, -1); deselectAll(list); dropDown(false); sendSelectionEvent(); } if (event.character == SWT.CR) { // Enter causes default selection dropDown(false); e = new Event(); e.time = event.time; e.stateMask = event.stateMask; notifyListeners(SWT.DefaultSelection, e); } // At this point the widget may have been disposed. // If so, do not continue. if (isDisposed()) { break; } e = new Event(); e.time = event.time; e.character = event.character; e.keyCode = event.keyCode; SWT_FACADE.copyEventKeyLocation(event, e); e.stateMask = event.stateMask; notifyListeners(SWT.KeyDown, e); event.doit = e.doit; break; case SWTFacade.MouseWheel: if (autoCompletionMode == AutoCompletionMode.FIRST_LETTER_MATCH) { // we handle the mouse wheel event ourselves and move the selection // up or down manually -- the default behavior scrolls the list // but does not move the selection! event.doit = false; if (SwtUtilities.isDisposed(list)) { return; } final int selection = getSelectionIndex(list); if (selection > -1) { final int newIndex = (event.count < 0) ? selection + 1 : selection - 1; if (newIndex > -1 && newIndex < getItemCount(list)) { setSelection(list, newIndex); } } else { setSelection(list, getTopIndex(list)); } } break; default: break; } } void popupEvent(final Event event) { switch (event.type) { case SWTFacade.Paint: // draw black rectangle around list final Rectangle listRect = list.getBounds(); final Color black = getDisplay().getSystemColor(SWT.COLOR_BLACK); event.gc.setForeground(black); event.gc.drawRectangle(0, 0, listRect.width + 1, listRect.height + 1); break; case SWT.Close: event.doit = false; dropDown(false); break; case SWT.Deactivate: // 337929: close drop down on Alt + TAB dropDown(false); break; default: break; } } @Override public void redraw() { super.redraw(); text.redraw(); arrow.redraw(); if (!SwtUtilities.isDisposed(popup) && popup.isVisible()) { list.redraw(); } } @Override public void redraw(final int x, final int y, final int width, final int height, final boolean all) { super.redraw(x, y, width, height, true); } /** * Removes all of the items from the receiver's list control and clear the contents of receiver's text field. * <p> * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that * created the receiver</li> * </ul> */ public void removeAll() { checkWidget(); clearImage(); text.setText(""); //$NON-NLS-1$ updateExtendedText(list, -1); removeAll(list); } /** * Removes the listener from the collection of listeners who will be notified when the receiver's text is modified. * * @param listener * the listener which should no longer be notified * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * * @see ModifyListener * @see #addModifyListener */ public void removeModifyListener(final ModifyListener listener) { checkWidget(); if (listener == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } removeListener(SWT.Modify, listener); } /** * Removes the listener from the collection of listeners who will be notified when the user changes the receiver's selection. * * @param listener * the listener which should no longer be notified * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * * @see SelectionListener * @see #addSelectionListener */ public void removeSelectionListener(final SelectionListener listener) { checkWidget(); if (listener == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } removeListener(SWT.Selection, listener); removeListener(SWT.DefaultSelection, listener); } /** * Removes the listener from the collection of listeners who will be notified when the control is verified. * * @param listener * the listener which should no longer be notified * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the listener is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * * @see VerifyListener * @see #addVerifyListener */ public void removeVerifyListener(final VerifyListener listener) { checkWidget(); if (listener == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } removeListener(SWT.Verify, listener); } /** * Adds the given {@link DropDownListener} to the listeners list. * <p> * IMPORTANT: This method is not API * * @since 5.0 */ public void removeDropDownListener(final DropDownListener listener) { dropDownListeners.remove(listener); } /** * Selects the item at the given zero-relative index in the receiver's list. If the item at the index was already selected, it remains selected. Indices * that are out of range are ignored. * * @param index * the index of the item to select * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void select(final int index) { checkWidget(); if (index == -1) { clearImage(); text.setText(""); //$NON-NLS-1$ updateExtendedText(list, -1); deselectAll(list); return; } if (0 <= index && index < getItemCount(list)) { if (index != getSelectionIndex()) { setImage(index); text.setText(getItem(list, index)); updateExtendedText(list, index); defaultTextSelection(); setSelection(list, index); } } } /** * Set's the strategy for autocompletion. See {@link AutoCompletionMode} for details. * <p> * The default value is {@link AutoCompletionMode#NO_MISSMATCH}. * * @param autoCompletionMode * an {@link AutoCompletionMode} instance; never null */ public void setAutoCompletionMode(final AutoCompletionMode autoCompletionMode) { Assert.isNotNull(autoCompletionMode); this.autoCompletionMode = autoCompletionMode; // hide system context menu so the user can not modify the text // by using the cut or paste action if (this.autoCompletionMode == AutoCompletionMode.ALLOW_MISSMATCH) { text.setMenu(systemContextMenu); } else { text.setMenu(emptyContextMenu); } } /** * {@inheritDoc} * <p> * Note: this will change the background of both the Text and List controls maintained by this control. */ @Override public void setBackground(final Color color) { super.setBackground(color); setTextBackground(color); setListBackground(color); } /** * Set the background of this Combo's List control. * * @param color * the new color (or null to set to the default system color) * @since 3.0 */ public void setListBackground(final Color color) { listBackground = color; if (list != null && !list.isDisposed()) { list.setBackground(color); } } /** * Set the background of this Combo's Text control. * * @param color * the new color (or null to set to the default system color) * @since 3.0 */ public void setTextBackground(final Color color) { if (label != null && !label.isDisposed()) { label.setBackground(color); } if (text != null && !text.isDisposed()) { text.setBackground(color); } if (arrow != null && !arrow.isDisposed()) { arrow.setBackground(color); } } /** * Sets the editable state. * * @param editable * the new editable state * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void setEditable(final boolean editable) { checkWidget(); text.setEditable(editable); arrow.setEnabled(isEnabled && editable); } /** * Sets the {@link IFlashDelegate} for this widget. * <p> * The IFlashDelegate is responsible for providing visual feedback on the control, when a user's keyboard entry is rejected. This happens when the combo is * configured to reject mismatching entries (AutoCompletionMode.NO_MISMATCH). When a rejection occurs the combo will notify the {@link IFlashDelegate}. * <p> * The default value is null. * * @see IFlashDelegate the {@link IFlashDelegate} to use with this combo. A null value indicates that no visual feedback is necessary. * @since 3.0 */ public void setFlashDelegate(final IFlashDelegate delegate) { this.flashDelegate = delegate; } @Override public void setEnabled(final boolean enabled) { this.isEnabled = enabled; super.setEnabled(enabled); text.setEnabled(enabled); final boolean editable = enabled && text.getEditable(); arrow.setEnabled(editable); if (!editable && !SwtUtilities.isDisposed(popup)) { popup.setVisible(editable); } } @Override public boolean setFocus() { checkWidget(); if (!isEnabled() || !isVisible()) { return false; } return text.setFocus(); } @Override public void setFont(final Font font) { super.setFont(font); this.font = font; text.setFont(font); list.setFont(font); internalLayout(true); } @Override public void setForeground(final Color color) { super.setForeground(color); listForeground = color; // fix for 304869 if (text != null && !text.isDisposed()) { text.setForeground(color); } if (list != null && !list.isDisposed()) { list.setForeground(color); } if (arrow != null && !arrow.isDisposed()) { arrow.setForeground(color); } } /** * Fills the combo's list control with the given array of items (with no images). * * @param items * an array of Strings for the items in the control; never null. Individual entries cannot be null. * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the items array is null</li> * <li>ERROR_INVALID_ARGUMENT - if an item in the items array is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> * @see #setItems(String[], Image[]) */ public void setItems(final String[] items) { setItems(items, null); } /** * Fills the combo's list control with the given array of items and images. * * @param items * an array of Strings for the items in the control; never null. Individual entries cannot be null. * @param images * an array of Images for the items in the control. May be null if no images should be used. Individual entries may be null, if no image should * be used for that item. * @throws RuntimeException * if the images and items arrays have different lengths * @since 3.0 */ public void setItems(final String[] items, final Image[] images) { checkWidget(); if (images != null) { Assert.isLegal(items.length == images.length, "Number of items and images does not match"); //$NON-NLS-1$ } setItems(list, items, images); for (final String item : items) { updateInputChars(item); } if (!text.getEditable()) { clearImage(); text.setText(""); //$NON-NLS-1$ updateExtendedText(list, -1); } } /** * Sets the layout which is associated with the receiver to be the argument which may be null. * <p> * Note: No Layout can be set on this Control because it already manages the size and position of its children. * </p> * * @param layout * the receiver's new layout or null * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ @Override public void setLayout(final Layout layout) { checkWidget(); return; } /** * Marks the receiver's list as visible if the argument is <code>true</code> , and marks it invisible otherwise. * <p> * If one of the receiver's ancestors is not visible or some other condition makes the receiver not visible, marking it visible may not actually cause it to * be displayed. * </p> * * @param visible * the new visibility state * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void setListVisible(final boolean visible) { checkWidget(); dropDown(visible); } @Override public void setMenu(final Menu menu) { text.setMenu(menu); } /** * Sets the selection in the receiver's text field to the range specified by the argument whose x coordinate is the start of the selection and whose y * coordinate is the end of the selection. * * @param selection * a point representing the new selection start and end * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the point is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void setSelection(final Point selection) { checkWidget(); if (selection == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } text.setSelection(selection.x, selection.y); } /** * Sets the contents of the receiver's text field to the given string. * <p> * Note: The text field in a <code>Combo</code> is typically only capable of displaying a single line of text. Thus, setting the text to a string containing * line breaks or other special characters will probably cause it to display incorrectly. * </p> * * @param string * the new text * * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the string is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void setText(final String string) { checkWidget(); if (string == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } final int index = indexOf(list, string, 0); if (index == -1) { deselectAll(list); clearImage(); text.setText(string); return; } setImage(index); text.setText(string); defaultTextSelection(); setSelection(list, index); updateExtendedText(list, index); } /** * Sets the maximum number of characters that the receiver's text field is capable of holding to be the argument. * * @param limit * new text limit * * @exception IllegalArgumentException * <ul> * <li>ERROR_CANNOT_BE_ZERO - if the limit is zero</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void setTextLimit(final int limit) { checkWidget(); text.setTextLimit(limit); } @Override public void setToolTipText(final String string) { checkWidget(); super.setToolTipText(string); arrow.setToolTipText(string); text.setToolTipText(string); } @Override public void setVisible(final boolean visible) { super.setVisible(visible); /* * At this point the control may have been disposed in a FocusOut event. If so then do not continue. */ if (isDisposed()) { return; } // TEMPORARY CODE if (popup == null || popup.isDisposed()) { return; } if (!visible) { popup.setVisible(false); } } /** * Sets the number of items that are visible in the drop down portion of the receiver's list. * * @param count * the new number of items to be visible * * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li> * </ul> */ public void setVisibleItemCount(final int count) { checkWidget(); if (count < 0) { return; } visibleItemCount = count; } void textEvent(final Event event) { switch (event.type) { case SWT.FocusIn: handleFocus(SWT.FocusIn); break; case SWT.DefaultSelection: dropDown(false); defaultTextSelection(); Event e = new Event(); e.time = event.time; e.stateMask = event.stateMask; notifyListeners(SWT.DefaultSelection, e); break; case SWT.DragDetect: case SWT.MouseDoubleClick: case SWTFacade.MouseMove: case SWTFacade.MouseEnter: case SWTFacade.MouseExit: case SWTFacade.MouseHover: Point pt = getDisplay().map(text, this, event.x, event.y); event.x = pt.x; event.y = pt.y; notifyListeners(event.type, event); event.type = SWT.None; break; case SWT.KeyDown: final Event keyEvent = new Event(); keyEvent.time = event.time; keyEvent.character = event.character; keyEvent.keyCode = event.keyCode; SWT_FACADE.copyEventKeyLocation(event, keyEvent); keyEvent.stateMask = event.stateMask; notifyListeners(SWT.KeyDown, keyEvent); if (isDisposed() || !getEditable()) { break; } if (autoCompletionMode == AutoCompletionMode.FIRST_LETTER_MATCH) { handleFirstLetterMatch(event); } else { handleAutoCompletion(event); } if (!event.doit) { break; } if (event.character == SWT.ESC) { // Escape key cancels popup list dropDown(false); break; } if (event.keyCode == SWT.ARROW_UP || event.keyCode == SWT.ARROW_DOWN) { event.doit = false; if ((event.stateMask & SWT.ALT) != 0) { final boolean dropped = isDropped(); defaultTextSelection(); if (!dropped) { setFocus(); } dropDown(!dropped); break; } final int oldIndex = getSelectionIndex(); if (event.keyCode == SWT.ARROW_UP) { select(Math.max(oldIndex - 1, 0)); } else { select(Math.min(oldIndex + 1, getItemCount() - 1)); } if (oldIndex != getSelectionIndex()) { e = new Event(); e.time = event.time; e.stateMask = event.stateMask; notifyListeners(SWT.Selection, e); } if (isDisposed()) { break; } } // Further work : Need to add support for incremental search in // pop up list as characters typed in text control break; case SWT.KeyUp: e = new Event(); e.time = event.time; e.character = event.character; e.keyCode = event.keyCode; SWT_FACADE.copyEventKeyLocation(event, e); e.stateMask = event.stateMask; notifyListeners(SWT.KeyUp, e); event.doit = e.doit; break; case SWT.MenuDetect: e = new Event(); e.time = event.time; notifyListeners(SWT.MenuDetect, e); break; case SWT.Modify: e = new Event(); e.time = event.time; notifyListeners(SWT.Modify, e); break; case SWT.MouseDown: pt = getDisplay().map(text, this, event.x, event.y); Event mouseEvent = new Event(); mouseEvent.button = event.button; mouseEvent.count = event.count; mouseEvent.stateMask = event.stateMask; mouseEvent.time = event.time; mouseEvent.x = pt.x; mouseEvent.y = pt.y; notifyListeners(SWT.MouseDown, mouseEvent); if (isDisposed()) { break; } event.doit = mouseEvent.doit; if (!event.doit) { break; } if (!isDropped()) { dropDown(true); ignoreFocusOut = true; setFocus(); ignoreFocusOut = false; } if (event.button != 1) { return; } if (text.getEditable()) { return; } final boolean dropped = isDropped(); defaultTextSelection(); if (!dropped) { setFocus(); } dropDown(!dropped); break; case SWT.MouseUp: pt = getDisplay().map(text, this, event.x, event.y); mouseEvent = new Event(); mouseEvent.button = event.button; mouseEvent.count = event.count; mouseEvent.stateMask = event.stateMask; mouseEvent.time = event.time; mouseEvent.x = pt.x; mouseEvent.y = pt.y; notifyListeners(SWT.MouseUp, mouseEvent); if (isDisposed()) { break; } event.doit = mouseEvent.doit; if (!event.doit) { break; } if (event.button != 1) { return; } if (text.getEditable()) { return; } defaultTextSelection(); break; case SWTFacade.MouseWheel: notifyListeners(SWTFacade.MouseWheel, event); event.type = SWT.None; if (isDisposed()) { break; } if (!event.doit) { break; } if (event.count != 0) { event.doit = false; final int oldIndex = getSelectionIndex(); if (event.count > 0) { select(Math.max(oldIndex - 1, 0)); } else { select(Math.min(oldIndex + 1, getItemCount() - 1)); } if (oldIndex != getSelectionIndex()) { e = new Event(); e.time = event.time; e.stateMask = event.stateMask; notifyListeners(SWT.Selection, e); } if (isDisposed()) { break; } } break; case SWT.Traverse: switch (event.detail) { case SWTFacade.TRAVERSE_ARROW_PREVIOUS: case SWTFacade.TRAVERSE_ARROW_NEXT: // The enter causes default selection and // the arrow keys are used to manipulate the list contents so // do not use them for traversal. event.doit = false; break; case SWT.TRAVERSE_TAB_PREVIOUS: event.doit = SWT_FACADE.traverse(this, SWT.TRAVERSE_TAB_PREVIOUS); event.detail = SWT.TRAVERSE_NONE; return; default: break; } e = new Event(); e.time = event.time; e.detail = event.detail; e.doit = event.doit; e.character = event.character; e.keyCode = event.keyCode; SWT_FACADE.copyEventKeyLocation(event, e); notifyListeners(SWT.Traverse, e); event.doit = e.doit; event.detail = e.detail; break; case SWT.Verify: e = new Event(); e.text = event.text; e.start = event.start; e.end = event.end; e.character = event.character; e.keyCode = event.keyCode; SWT_FACADE.copyEventKeyLocation(event, e); e.stateMask = event.stateMask; notifyListeners(SWT.Verify, e); event.text = e.text; event.doit = e.doit; break; default: break; } } // helping methods ////////////////// private String buildPrefixOnBackSpace(final Point selection) { String result; final String theText = text.getText(); if (isAllowMissmatch()) { final int end = Math.max(0, selection.x - 1); result = theText.substring(0, end) + theText.substring(selection.y); } else { final String prefix = theText.substring(0, selection.x); final int end = Math.max(0, prefix.length() - 1); result = prefix.substring(0, end); } return result; } private String buildPrefixForInput(final char typedChar, final Point selection) { String prefix = text.getText().substring(0, selection.x); if (isAllowMissmatch()) { prefix = text.getText().substring(0, selection.x) + typedChar + text.getText().substring(selection.y); } else { prefix += typedChar; } return prefix; } /** * @since 6.0 */ protected void clearImage() { if (label != null) { label.setImage(null); } } /** * 337929: close drop down on Alt + TAB */ private void closeDropDownAfterDeactivatingShell() { final Display display = getDisplay(); // has to be async, to allow the shell switch to complete display.asyncExec(new Runnable() { public void run() { if (display.getActiveShell() == null) { dropDown(false); } } }); } private void defaultTextSelection() { if (autoCompletionMode == AutoCompletionMode.FIRST_LETTER_MATCH) { text.setSelection(0, 0); } else { text.selectAll(); } } private String getFirstCharFromTextWidget() { String result = null; if (text.getText().length() > 0) { result = text.getText().substring(0, 1); } return result; } /** * Handles a key press when one of these autocompletion modes is set: * <ul> * <li>AutoCompletionMode.NO_MISSMATCH</li> * <li>AutoCompletionMode.ALLOW_MISSMATCH</li> * </ul> * * @param event * the key event that triggered the method */ private void handleAutoCompletion(final Event event) { event.doit = false; // System.out.println(String.format("ch:%c, kc:%d, mask:%d", event.character, event.keyCode, event.stateMask)); if (event.stateMask == SWT.ALT || event.stateMask == SWT.CONTROL) { // Must use equality instead of (stateMask & const > 0) here! // This allows ALT + x or CONTROL + x combos. However // ALT + CONTROL + x will not go in here and will be handled // by isInputChar(x) instead. event.doit = handleClipboardOperations(event); } else if (event.character == SWT.DEL && isAllowMissmatch()) { event.doit = true; } else if (isControlChar(event)) { // System.out.println("isControlChar: " + event.character); event.doit = true; } else if (isInputChar(event.character)) { // System.out.println("isInputChar: " + event.character); final Point selection = getSelection(); final String newPrefix; if (event.character == SWT.BS) { newPrefix = buildPrefixOnBackSpace(selection); } else { newPrefix = buildPrefixForInput(event.character, selection); } final boolean matched = matchPrefixWithList(newPrefix); // System.out.println("prefix: '" + newPrefix + "' matched? " + matched); if (!matched) { if (isAllowMissmatch()) { clearImage(); if (getSelectionIndex() != -1) { deselectAll(list); sendSelectionEvent(); } event.doit = true; } else { if (flashDelegate != null) { flashDelegate.flash(); } } } } } private boolean handleClipboardOperations(final Event event) { boolean result = true; if (event.stateMask == SWT.CONTROL) { if (event.keyCode == 118) { // Ctrl + V handlePaste(); result = false; } else if (event.keyCode == 120) { // Ctrl + X handleCut(); result = false; } } return result; } private void handleCut() { final Point selection = text.getSelection(); final String oldText = text.getText(); final String newText = oldText.substring(0, selection.x) + oldText.substring(selection.y, oldText.length()); final int index = indexOf(newText); if (index != -1 || newText.length() == 0) { ClipboardFacade.getDefault().cut(text); if (index == -1) { clearImage(); } else { setImage(index); } text.setText(newText); text.setSelection(newText.length()); setSelection(list, index); updateExtendedText(list, index); sendSelectionEvent(); } else if (index == -1 && isAllowMissmatch()) { clearImage(); text.setText(newText); text.setSelection(newText.length()); updateExtendedText(list, index); // TODO ??? } } /** * Handles a key press when the AutoCompletionMode.FIRST_LETTER_MATCH is set. * * @param event * the key event that triggered the method */ private void handleFirstLetterMatch(final Event event) { event.doit = false; // System.out.println(String.format("ch:%c, kc:%d, mask:%d", event.character, event.keyCode, event.stateMask)); if (event.stateMask == SWT.ALT || event.stateMask == SWT.CONTROL) { // Must use equality instead of (stateMask & const > 0) here! // This allows ALT + x or CONTROL + x combos. However // ALT + CONTROL + x will not go in here and will be handled // by isInputChar(x) instead. event.doit = handleClipboardOperations(event); } else if (isControlChar(event)) { // System.out.println("isControlChar: " + event.character); event.doit = true; } else if (isInputChar(event.character)) { boolean matched = false; int startIndex = 0; final String keyChar = String.valueOf(event.character); final String firstChar = getFirstCharFromTextWidget(); if (keyChar.equalsIgnoreCase(firstChar)) { startIndex = getSelectionIndex() + 1; } final String prefix = String.valueOf(event.character); final String[] items = getItems(list); // search beneath startIndex for (int i = startIndex; i < items.length; i++) { final String item = items[i]; if (matchesWord(prefix, item)) { setMatchingTextAndSelection(0, 0, item); matched = true; break; } } // 335126: if no result, then search above startIndex for (int i = 0; !matched && i < startIndex; i++) { final String item = items[i]; if (matchesWord(prefix, item)) { setMatchingTextAndSelection(0, 0, item); matched = true; break; } } if (!matched) { if (flashDelegate != null) { flashDelegate.flash(); } } } } private void handlePaste() { final String data = ClipboardFacade.getDefault().getTextFromClipboard(getDisplay()); if (data != null) { final Point selection = text.getSelection(); final String oldText = text.getText(); final String newText = oldText.substring(0, selection.x) + data + oldText.substring(selection.y, oldText.length()); final int index = indexOf(newText); if (index != -1) { setImage(index); text.setText(newText); text.setSelection(newText.length()); setSelection(list, index); sendSelectionEvent(); updateExtendedText(list, index); } else if (index == -1 && isAllowMissmatch()) { clearImage(); text.setText(newText); text.setSelection(newText.length()); updateExtendedText(list, index); // TODO ??? } } } private boolean isAllowMissmatch() { return autoCompletionMode == AutoCompletionMode.ALLOW_MISSMATCH; } private boolean isControlChar(final Event event) { final char[] chars = { SWT.ESC, SWT.CR }; for (final int ch : chars) { if (ch == event.character) { return true; } } final int[] keyCodes = { SWT.ARROW_UP, SWT.ARROW_DOWN, SWT.ARROW_LEFT, SWT.ARROW_RIGHT, SWT.HOME, SWT.END }; for (final int keycode : keyCodes) { if (keycode == event.keyCode) { return true; } } return false; } private boolean isInputChar(final char ch) { if (Character.isLetterOrDigit(ch) || ch == SWT.BS) { return true; } final Character character = Character.valueOf(ch); if (inputChars.contains(character)) { return true; } if (isAllowMissmatch()) { // Special character: dash, punctuation, currency, quotes, brackets, ... final int type = Character.getType(ch); if (type == Character.LETTER_NUMBER || type == Character.OTHER_NUMBER || type >= Character.DASH_PUNCTUATION && type < Character.FINAL_QUOTE_PUNCTUATION) { return true; } } return false; } private boolean matchesWord(final String prefix, final String word) { if (prefix == null || word == null) { return false; } if (word.toLowerCase().startsWith(prefix.toLowerCase())) { return true; } return false; } private boolean matchPrefixWithList(final String prefix) { boolean result = false; if (prefix != null) { if (prefix.length() == 0) { clearImage(); text.setText(""); //$NON-NLS-1$ updateExtendedText(list, -1); if (getSelectionIndex() > -1) { deselectAll(list); sendSelectionEvent(); } result = true; } else { for (final String item : getItems(list)) { if (matchesWord(prefix, item)) { setMatchingTextAndSelection(prefix.length(), item.length(), item); result = true; break; } } } } return result; } /** * @since 6.0 */ protected void updateInputChars(final String string) { for (int i = 0; i < string.length(); i++) { final char ch = string.charAt(i); if (!Character.isLetterOrDigit(ch)) { inputChars.add(Character.valueOf(ch)); } } } private void sendSelectionEvent() { final Event event = new Event(); event.widget = this; event.type = SWT.Selection; notifyListeners(event.type, event); } private void setImage(final int index) { if (!SwtUtilities.isDisposed(label)) { label.setImage(getImage(list, index)); } } private void setMatchingTextAndSelection(final int selectionStart, final int selectionEnd, final String item) { final int index = indexOf(item); Assert.isLegal(index > -1); setImage(index); text.setText(item); text.setSelection(selectionStart, selectionEnd); setSelection(list, index); updateExtendedText(list, index); sendSelectionEvent(); } }