/******************************************************************************* * Copyright (c) 2013 Oak Ridge National Laboratory. * 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 ******************************************************************************/ package org.csstudio.ui.util.widgets; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.eclipse.swt.SWT; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; /** * Combo-type widget that allows selecting multiple items. * * <p> * Takes a list of {@link Object}s as input. * * <p> * The <code>toString()</code> of each Object is displayed in a drop-down list. * Overriding the stringRepresention() method, the user can define an * alternative way to convert T to String. * * <p> * One or more items can be selected, they're also displayed in the text field. * * <p> * Items can be entered in the text field, comma-separated. If entered text does * not match a valid item, text is highlighted and tool-tip indicates error. * * <p> * Keyboard support: 'Down' key in text field opens drop-down. Inside drop-down, * single item can be selected via cursor key and 'RETURN' closes the drop-down. * * TODO Auto-completion while typing? * * @author Kay Kasemir, Kunal Shroff */ public class MultipleSelectionCombo<T> extends Composite { final private static String SEPARATOR = ", "; //$NON-NLS-1$ final private static String SEPERATOR_PATTERN = "\\s*,\\s*"; //$NON-NLS-1$ private final PropertyChangeSupport changeSupport = new PropertyChangeSupport( this); private Display display; private Text text; /** Pushing the drop_down button opens the popup */ private Button drop_down; private Shell popup; private org.eclipse.swt.widgets.List list; /** Items to show in list */ private List<T> items = new ArrayList<T>(); /** Selection indices */ private List<Integer> selectionIndex = new ArrayList<Integer>(); private String tool_tip = null; private Color text_color = null; /** * When list looses focus, the event time is noted here. This prevents the * drop-down button from re-opening the list right away. */ private long lost_focus = 0; private volatile boolean modify = false; /** * Initialize * * @param parent * @param style */ public MultipleSelectionCombo(final Composite parent, final int style) { super(parent, style); createComponents(parent); } /** * Create SWT components * * @param parent */ private void createComponents(final Composite parent) { display = parent.getDisplay(); final GridLayout layout = new GridLayout(); layout.numColumns = 2; layout.marginWidth = 0; setLayout(layout); addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent e) { switch (e.getPropertyName()) { case "selection": if (modify) { break; } else { updateText(); break; } case "items": setSelection(Collections.<T> emptyList()); break; default: break; } } }); text = new Text(this, SWT.BORDER); GridData gd = new GridData(SWT.FILL, 0, true, false); text.setLayoutData(gd); text.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { // Analyze text, update selection final String items_text = text.getText(); modify = true; setSelection(items_text); modify = false; } }); text.addKeyListener(new KeyAdapter() { @Override public void keyPressed(final KeyEvent e) { switch (e.keyCode) { case SWT.ARROW_DOWN: drop(true); return; case SWT.ARROW_UP: drop(false); return; case SWT.CR: modify =false; updateText(); } } }); drop_down = new Button(this, SWT.ARROW | SWT.DOWN); gd = new GridData(SWT.FILL, SWT.FILL, false, false); gd.heightHint = text.getBounds().height; drop_down.setLayoutData(gd); drop_down.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent e) { // Was list open, user clicked this button to close, // and list self-closed because is lost focus? // e.time is an unsigned integer and should be AND'ed with // 0xFFFFFFFFL so that it can be treated as a signed long. if ((e.time & 0xFFFFFFFFL) - lost_focus <= 300) return; // Done // If list is not open, open it if (!isDropped()) drop(true); } }); } /** {@inheritDoc} */ @Override public void setForeground(final Color color) { text_color = color; text.setForeground(color); } /** {@inheritDoc} */ @Override public void setToolTipText(final String tooltip) { tool_tip = tooltip; text.setToolTipText(tooltip); drop_down.setToolTipText(tooltip); } /** * Define items to be displayed in the list, and returned as the current * selection when selected. * * @param new_items * Items to display in the list */ public void setItems(final List<T> items) { List<T> oldValue = this.items; this.items = items; changeSupport.firePropertyChange("items", oldValue, this.items); } /** * Get the list of items * * @return list of selectable items */ public List<T> getItems() { return this.items; } /** * Set items that should be selected. * * <p> * Selected items must be on the list of items provided via * <code>setItems</code> * * @param sel_items * Items to select in the list */ public void setSelection(final List<T> selection) { List<Integer> oldValue = this.selectionIndex; List<Integer> newSelectionIndex = new ArrayList<Integer>( selection.size()); for (T t : selection) { int index = items.indexOf(t); if (index >= 0) { newSelectionIndex.add(items.indexOf(t)); } } this.selectionIndex = newSelectionIndex; changeSupport.firePropertyChange("selection", oldValue, this.selectionIndex); } /** * set the items to be selected, the selection is specified as a string with * values separated by {@value MultipleSelectionCombo.SEPARATOR} * * @param selection_text * Items to select in the list as comma-separated string */ public void setSelection(final String selection) { setSelection("".equals(selection) ? new String[0] : selection .split(SEPERATOR_PATTERN)); } /** * Set the items to be selected * * @param selections */ public void setSelection(final String[] selections) { List<Integer> oldValue = this.selectionIndex; List<Integer> newSelectionIndex; if (selections.length > 0) { newSelectionIndex = new ArrayList<Integer>(selections.length); // Locate index for each item for (String item : selections) { int index = getIndex(item); if (index >= 0 && index < items.size()) { newSelectionIndex.add(getIndex(item)); text.setForeground(text_color); text.setToolTipText(tool_tip); } else { text.setForeground(display.getSystemColor(SWT.COLOR_RED)); text.setToolTipText("Text contains invalid items"); } } } else { newSelectionIndex = Collections.emptyList(); } this.selectionIndex = newSelectionIndex; changeSupport.firePropertyChange("selection", oldValue, this.selectionIndex); } /** * return the index of the object in items with the string representation * _string_ * * @param string * @return */ private Integer getIndex(String string) { for (T item : items) { if (stringRepresention(item).equals(string)) { return items.indexOf(item); } } return -1; } /** * get the list of items currently selected. Note: this does not return the * list in the order of selection. * * @return the list of selected items */ public List<T> getSelection() { List<T> selection = new ArrayList<T>(this.selectionIndex.size()); for (int index : this.selectionIndex) { selection.add(items.get(index)); } return Collections.unmodifiableList(selection); } /** Update <code>selection</code> from <code>list</code> */ private void updateSelectionFromList() { setSelection(list.getSelection()); } /** Update <code>text</code> to reflect <code>selection</code> */ private void updateText() { final StringBuilder buf = new StringBuilder(); for (Integer index : selectionIndex) { if (buf.length() > 0) buf.append(SEPARATOR); buf.append(stringRepresention(items.get(index))); } text.setText(buf.toString()); text.setSelection(buf.length()); } /** @return <code>true</code> if drop-down is visible */ private boolean isDropped() { return popup != null; } /** * @param drop * Display drop-down? */ private void drop(boolean drop) { if (drop == isDropped()) return; if (drop) createPopup(); else hidePopup(); } /** Create shell that simulates drop-down */ private void createPopup() { popup = new Shell(getShell(), SWT.NO_TRIM | SWT.ON_TOP); popup.setLayout(new FillLayout()); list = new org.eclipse.swt.widgets.List(popup, SWT.MULTI | SWT.V_SCROLL); list.setToolTipText(tool_tip); // Position popup under the text box Rectangle bounds = text.getBounds(); bounds.y += bounds.height; // As high as necessary for items bounds.height = 5 + 2 * list.getBorderWidth() + list.getItemHeight() * items.size(); // ..with limitation bounds.height = Math.min(bounds.height, display.getBounds().height / 2); // Map to screen coordinates bounds = display.map(text, null, bounds); popup.setBounds(bounds); popup.open(); // Update text from changed list selection list.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(SelectionEvent e) { updateSelectionFromList(); updateText(); } @Override public void widgetDefaultSelected(SelectionEvent e) { updateSelectionFromList(); updateText(); hidePopup(); } }); String[] stringItems = new String[items.size()]; for (int i = 0; i < items.size(); i++) { stringItems[i] = stringRepresention(items.get(i)); } list.setItems(stringItems); int[] intSelectionIndex = new int[selectionIndex.size()]; for (int i = 0; i < intSelectionIndex.length; i++) { intSelectionIndex[i] = selectionIndex.get(i); } list.setSelection(intSelectionIndex); list.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { if (e.keyCode == SWT.CR) { hidePopup(); } } }); // Hide popup when loosing focus list.addFocusListener(new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { // This field is an unsigned integer and should be AND'ed with // 0xFFFFFFFFL so that it can be treated as a signed long. lost_focus = e.time & 0xFFFFFFFFL; hidePopup(); } }); list.setFocus(); } /** Hide popup shell */ private void hidePopup() { if (popup != null) { popup.close(); popup.dispose(); popup = null; } text.setFocus(); } /** * Register a PropertyChangeListener on this widget. the listener will be * notified when the items or the selection is changed. * * @param listener */ public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } /** * remove the PropertyChangeListner * * @param listener */ public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } /** * Override this method to define the how the object should be represented * as a string. * * @param object * @return the string representation of the object */ public String stringRepresention(T object) { return object.toString(); } }