blob: 43f63b217e450c508ec2771aac0c33723bfdb29e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 Original authors 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:
* Original authors and others - initial API and implementation
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 454111
* Ryan McHale <rpmc22@gmail.com> - Bug 484716
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.widget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.nebula.widgets.nattable.edit.EditConstants;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.CellStyleUtil;
import org.eclipse.nebula.widgets.nattable.style.HorizontalAlignmentEnum;
import org.eclipse.nebula.widgets.nattable.style.IStyle;
import org.eclipse.nebula.widgets.nattable.style.VerticalAlignmentEnum;
import org.eclipse.nebula.widgets.nattable.ui.matcher.LetterOrDigitKeyEventMatcher;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
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.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
/**
* Customized combobox control that supports editing directly in the text field
* and selecting items from the dropdown.
*
* <p>
* This control supports the ability for multi select in the dropdown of the
* combo which is not available for the SWT Combo control. This feature was
* added with Nebula NatTable 1.0.0
*
* <p>
* The following style bits are supported by this control.
*
* @see SWT#BORDER (if a border should be added to the Text control)
* @see SWT#READ_ONLY (default for Text control, if this is missing, the Text
* control can be edited)
* @see SWT#CHECK (if the items in the combo should be showed with checkboxes)
* @see SWT#MULTI (if multi selection is allowed)
*/
public class NatCombo extends Composite {
/**
* Default String that is used to separate values in the String
* representation showed in the text control if multiselect is supported.
*/
public static final String DEFAULT_MULTI_SELECT_VALUE_SEPARATOR = ", "; //$NON-NLS-1$
/**
* Default String that is used to prefix the generated String representation
* showed in the text control if multiselect is supported.
*/
public static final String DEFAULT_MULTI_SELECT_PREFIX = "["; //$NON-NLS-1$
/**
* String that is used to suffix the generated String representation showed
* in the text control if multiselect is supported.
*/
public static final String DEFAULT_MULTI_SELECT_SUFFIX = "]"; //$NON-NLS-1$
/**
* The default number of visible items on open the combo.
*/
public static final int DEFAULT_NUM_OF_VISIBLE_ITEMS = 5;
/**
* The IStyle that is used for rendering the Text and the combo control. The
* important configurations used are horizontal alignment, background and
* foreground color and font.
*/
protected final IStyle cellStyle;
/**
* The maximum number of visible items of the combo. Setting this value to
* -1 will result in always showing all items at once.
*/
protected int maxVisibleItems;
/**
* The items that are showed within the combo transformed to a
* java.util.List. Needed for indexed operations in the dropdown
*/
protected java.util.List<String> itemList;
/**
* Map used to hold the selection state of items in the drop. Needed to
* maintain state when filtering
*
* @since 1.4
*/
protected Map<String, Boolean> selectionStateMap;
/**
* The text control allowing filtering of options
*
* @since 1.4
*/
protected Text filterBox;
/**
* The text control of this NatCombo, allowing to enter values directly.
*/
protected Text text;
/**
* The Shell containing the dropdown of this NatCombo
*/
protected Shell dropdownShell;
/**
* The Table control used for the combo component of this NatCombo
*/
protected Table dropdownTable;
/**
* The Table control used for the combo component of this NatCombo
*
* @since 1.4
*/
protected TableViewer dropdownTableViewer;
/**
* The image that is shown at the right edge of the text control if the
* NatCombo is opened.
*/
protected Image iconImage;
/**
* The style bits that where set on creation time. Needed in case the
* dropdown shell was disposed and needs to be created again.
*
* @since 2.0 - Renamed from style to widgetStyle to avoid possible
* confusions with Widget#style.
*/
protected final int widgetStyle;
/**
* Flag that indicated whether this NatCombo supports filtering of the
* values in the dropdown control
*
* @since 1.4
*/
protected boolean showDropdownFilter;
/**
* Flag that indicates whether this ComboBoxCellEditor supports free editing
* in the text control of the NatCombo or not. By default free editing is
* disabled.
*/
protected boolean freeEdit;
/**
* Flag that indicates whether this NatCombo supports multiselect or not. By
* default multiselect is disabled.
*/
protected boolean multiselect;
/**
* Flag that indicates whether checkboxes should be shown for the items in
* the dropdown.
*/
protected boolean useCheckbox;
/**
* String that is used to separate values in the String representation
* showed in the text control if multiselect is supported.
*/
protected String multiselectValueSeparator = DEFAULT_MULTI_SELECT_VALUE_SEPARATOR;
/**
* String that is used to prefix the generated String representation showed
* in the text control if multiselect is supported. Needed to visualize the
* multiselection to the user.
*/
protected String multiselectTextPrefix = DEFAULT_MULTI_SELECT_PREFIX;
/**
* String that is used to suffix the generated String representation showed
* in the text control if multiselect is supported. Needed to visualize the
* multiselection to the user.
*/
protected String multiselectTextSuffix = DEFAULT_MULTI_SELECT_SUFFIX;
/**
* Flag that tells whether the NatCombo has focus or not. The flag is set by
* the FocusListenerWrapper that is set as focus listener on both, the Text
* control and the dropdown table control. This flag is necessary as the
* NatCombo has focus if either of both controls have focus.
*/
private boolean hasFocus = false;
/**
* The list of FocusListener that contains the listeners that will be
* informed if the NatCombo control gains or looses focus. We keep our own
* list of listeners because the two controls that are combined in this
* control share the same focus.
*/
private List<FocusListener> focusListener = new ArrayList<FocusListener>();
/**
* List of KeyListener that should be added to the dropdown table once it is
* created. Kept locally because the table creation is deferred to the first
* access.
*/
private List<KeyListener> keyListener = new ArrayList<KeyListener>();
/**
* List of TraverseListener that should be added to the dropdown table once
* it is created. Kept locally because the table creation is deferred to the
* first access.
*/
private List<TraverseListener> traverseListener = new ArrayList<TraverseListener>();
/**
* List of MouseListener that should be added to the dropdown table once it
* is created. Kept locally because the table creation is deferred to the
* first access.
*/
private List<MouseListener> mouseListener = new ArrayList<MouseListener>();
/**
* List of SelectionListener that should be added to the dropdown table once
* it is created. Kept locally because the table creation is deferred to the
* first access.
*/
private List<SelectionListener> selectionListener = new ArrayList<SelectionListener>();
/**
* List of ShellListener that should be added to the dropdown table once it
* is created. Kept locally because the table creation is deferred to the
* first access.
*/
private List<ShellListener> shellListener = new ArrayList<ShellListener>();
/**
* Creates a new NatCombo using the given IStyle for rendering, showing the
* default number of items at once in the dropdown. Creating the NatCombo
* with this constructor, there is no free edit and no multiple selection
* enabled.
*
* @param parent
* A widget that will be the parent of this NatCombo
* @param cellStyle
* Style configuration containing horizontal alignment, font,
* foreground and background color information.
* @param style
* The style for the Text Control to construct. Uses this style
* adding internal styles via ConfigRegistry.
*/
public NatCombo(Composite parent, IStyle cellStyle, int style) {
this(parent, cellStyle, DEFAULT_NUM_OF_VISIBLE_ITEMS, style, GUIHelper.getImage("down_2")); //$NON-NLS-1$
}
/**
* Creates a new NatCombo using the given IStyle for rendering, showing the
* given amount of items at once in the dropdown. Creating the NatCombo with
* this constructor, there is no free edit and no multiple selection
* enabled.
*
* @param parent
* A widget that will be the parent of this NatCombo
* @param cellStyle
* Style configuration containing horizontal alignment, font,
* foreground and background color information.
* @param maxVisibleItems
* the max number of items the drop down will show before
* introducing a scroll bar.
* @param style
* The style for the Text Control to construct. Uses this style
* adding internal styles via ConfigRegistry.
*/
public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style) {
this(parent, cellStyle, maxVisibleItems, style, GUIHelper.getImage("down_2")); //$NON-NLS-1$
}
/**
* Creates a new NatCombo using the given IStyle for rendering, showing the
* given amount of items at once in the dropdown. Creating the NatCombo with
* this constructor, there is no free edit and no multiple selection
* enabled.
*
* @param parent
* A widget that will be the parent of this NatCombo
* @param cellStyle
* Style configuration containing horizontal alignment, font,
* foreground and background color information.
* @param maxVisibleItems
* the max number of items the drop down will show before
* introducing a scroll bar.
* @param style
* The style for the Text Control to construct. Uses this style
* adding internal styles via ConfigRegistry.
*
* @param showDropdownFilter
* Flag indicating whether the dropdown filter is displayed
*
* @since 1.4
*/
public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style, boolean showDropdownFilter) {
this(parent, cellStyle, maxVisibleItems, style, GUIHelper.getImage("down_2"), showDropdownFilter); //$NON-NLS-1$
}
/**
* Creates a new NatCombo using the given IStyle for rendering, showing the
* given amount of items at once in the dropdown.
*
* @param parent
* A widget that will be the parent of this NatCombo
* @param cellStyle
* Style configuration containing horizontal alignment, font,
* foreground and background color information.
* @param maxVisibleItems
* the max number of items the drop down will show before
* introducing a scroll bar.
* @param style
* The style for the {@link Text} Control to construct. Uses this
* style adding internal styles via ConfigRegistry.
* @param iconImage
* The image to use as overlay to the {@link Text} Control if the
* dropdown is visible. Using this image will indicate that the
* control is an open combo to the user.
*/
public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style, Image iconImage) {
this(parent, cellStyle, maxVisibleItems, style, GUIHelper.getImage("down_2"), false); //$NON-NLS-1$
}
/**
* Creates a new NatCombo using the given IStyle for rendering, showing the
* given amount of items at once in the dropdown.
*
* @param parent
* A widget that will be the parent of this NatCombo
* @param cellStyle
* Style configuration containing horizontal alignment, font,
* foreground and background color information.
* @param maxVisibleItems
* the max number of items the drop down will show before
* introducing a scroll bar.
* @param style
* The style for the {@link Text} Control to construct. Uses this
* style adding internal styles via ConfigRegistry.
* @param iconImage
* The image to use as overlay to the {@link Text} Control if the
* dropdown is visible. Using this image will indicate that the
* control is an open combo to the user.
*
* @param showDropdownFilter
* Flag indicating whether the dropdown filter is displayed
*
* @since 1.4
*/
public NatCombo(Composite parent, IStyle cellStyle, int maxVisibleItems, int style, Image iconImage, boolean showDropdownFilter) {
super(parent, SWT.NONE);
this.cellStyle = cellStyle;
this.maxVisibleItems = maxVisibleItems;
this.iconImage = iconImage;
this.widgetStyle = style;
this.showDropdownFilter = showDropdownFilter;
this.freeEdit = (style & SWT.READ_ONLY) == 0;
this.multiselect = (style & SWT.MULTI) != 0;
this.useCheckbox = (style & SWT.CHECK) != 0;
GridLayout gridLayout = new GridLayout(2, false);
gridLayout.marginWidth = 0;
gridLayout.marginHeight = 0;
gridLayout.horizontalSpacing = 0;
setLayout(gridLayout);
createTextControl(style);
// typically the dropdown shell should be hidden when the focus is lost
// but in case the NatCombo is the first control in a shell, the text
// control will get the focus immediately after the shell lost focus.
// as handling with focus listeners in such a case fails, we add a move
// listener that will update the position of the dropdown shell if the
// parent shell moves
final Listener moveListener = new Listener() {
@Override
public void handleEvent(Event event) {
calculateBounds();
}
};
getShell().addListener(SWT.Move, moveListener);
addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
if (NatCombo.this.dropdownShell != null) {
NatCombo.this.dropdownShell.dispose();
}
NatCombo.this.text.dispose();
NatCombo.this.getShell().removeListener(SWT.Move, moveListener);
}
});
}
/**
* Sets the given items to be the items shown in the dropdown of this
* NatCombo.
*
* @param items
* The array of items to set.
*/
public void setItems(String[] items) {
if (items != null) {
this.itemList = Arrays.asList(items);
this.selectionStateMap = new LinkedHashMap<String, Boolean>();
for (String item : items) {
this.selectionStateMap.put(item, Boolean.FALSE);
}
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTableViewer.setInput(items);
}
}
}
/**
* Creates the Text control of this NatCombo, adding styles, look&amp;feel
* and needed listeners for the control only.
*
* @param style
* The style for the Text Control to construct. Uses this style
* adding internal styles via ConfigRegistry.
*/
protected void createTextControl(int style) {
int textStyle = style | HorizontalAlignmentEnum.getSWTStyle(this.cellStyle);
this.text = new Text(this, textStyle);
this.text.setBackground(this.cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR));
this.text.setForeground(this.cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR));
this.text.setFont(this.cellStyle.getAttributeValue(CellStyleAttributes.FONT));
GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
this.text.setLayoutData(gridData);
this.text.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent event) {
if (event.keyCode == SWT.ARROW_DOWN
|| event.keyCode == SWT.ARROW_UP) {
showDropdownControl();
int selectionIndex = getDropdownTable().getSelectionIndex();
if (selectionIndex < 0) {
select(0);
} else {
// only visualize the selection in the dropdown, do not
// perform a selection
getDropdownTable().select(selectionIndex);
}
// ensure the arrow key events do not have any further
// effect
event.doit = false;
} else if (!LetterOrDigitKeyEventMatcher.isLetterOrDigit(event.character)) {
if (NatCombo.this.freeEdit) {
// simply clear the selection in dropdownlist so the
// free value in text control will be used
if (!getDropdownTable().isDisposed()) {
getDropdownTable().deselectAll();
for (Map.Entry<String, Boolean> entry : NatCombo.this.selectionStateMap.entrySet()) {
entry.setValue(Boolean.FALSE);
}
}
} else {
showDropdownControl();
}
}
}
});
this.text.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
if (!NatCombo.this.freeEdit) {
if (getDropdownTable().isDisposed()
|| !getDropdownTable().isVisible()) {
showDropdownControl();
} else {
// if there is no free edit enabled, set the focus back
// to the dropdownlist so it handles key strokes itself
getDropdownTable().forceFocus();
}
}
}
});
this.text.addControlListener(new ControlListener() {
@Override
public void controlResized(ControlEvent e) {
calculateBounds();
}
@Override
public void controlMoved(ControlEvent e) {
calculateBounds();
}
});
this.text.addFocusListener(new FocusListenerWrapper());
final Canvas iconCanvas = new Canvas(this, SWT.NONE) {
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
Rectangle iconImageBounds = NatCombo.this.iconImage.getBounds();
return new Point(iconImageBounds.width + 2, iconImageBounds.height + 2);
}
};
gridData = new GridData(GridData.BEGINNING, SWT.FILL, false, true);
iconCanvas.setLayoutData(gridData);
iconCanvas.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent event) {
GC gc = event.gc;
Rectangle iconCanvasBounds = iconCanvas.getBounds();
Rectangle iconImageBounds = NatCombo.this.iconImage.getBounds();
int horizontalAlignmentPadding =
CellStyleUtil.getHorizontalAlignmentPadding(
HorizontalAlignmentEnum.CENTER, iconCanvasBounds, iconImageBounds.width);
int verticalAlignmentPadding =
CellStyleUtil.getVerticalAlignmentPadding(
VerticalAlignmentEnum.MIDDLE, iconCanvasBounds, iconImageBounds.height);
gc.drawImage(NatCombo.this.iconImage, horizontalAlignmentPadding, verticalAlignmentPadding);
Color originalFg = gc.getForeground();
gc.setForeground(GUIHelper.COLOR_WIDGET_BORDER);
gc.drawRectangle(0, 0, iconCanvasBounds.width - 1, iconCanvasBounds.height - 1);
gc.setForeground(originalFg);
}
});
iconCanvas.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
if (NatCombo.this.dropdownShell != null && !NatCombo.this.dropdownShell.isDisposed()) {
if (NatCombo.this.dropdownShell.isVisible()) {
NatCombo.this.text.forceFocus();
hideDropdownControl();
} else {
showDropdownControl();
}
} else {
showDropdownControl();
}
}
});
}
/**
* Create the dropdown control of this NatCombo, adding styles,
* look&amp;feel and needed listeners for the control only.
*
* @param style
* The style for the Table Control to construct. Uses this style
* adding internal styles via ConfigRegistry.
*/
protected void createDropdownControl(int style) {
this.dropdownShell = new Shell(getShell(), SWT.MODELESS);
// (SWT.V_SCROLL | SWT.NO_SCROLL) prevents appearance of unnecessary
// horizontal scrollbar on mac
// see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=304128
int scrollStyle = ((this.itemList != null && this.itemList.size() > this.maxVisibleItems)
&& this.maxVisibleItems > 0) ? (SWT.V_SCROLL | SWT.NO_SCROLL) : SWT.NO_SCROLL;
int dropdownListStyle = style
| scrollStyle
| HorizontalAlignmentEnum.getSWTStyle(this.cellStyle)
| SWT.FULL_SELECTION;
this.dropdownTable = new Table(this.dropdownShell, dropdownListStyle);
this.dropdownTableViewer = new TableViewer(this.dropdownTable);
this.dropdownTable.setBackground(
this.cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR));
this.dropdownTable.setForeground(
this.cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR));
this.dropdownTable.setFont(
this.cellStyle.getAttributeValue(CellStyleAttributes.FONT));
// add a column to be able to resize the item width in the dropdown
new TableColumn(this.dropdownTable, SWT.NONE);
this.dropdownTableViewer.setContentProvider(ArrayContentProvider.getInstance());
this.dropdownTableViewer.setLabelProvider(new LabelProvider() {
@Override
public boolean isLabelProperty(Object element, String property) {
return false;
}
});
this.dropdownTable.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent event) {
if (event.keyCode == SWT.ARROW_DOWN) {
int selected = NatCombo.this.dropdownTable.getSelectionIndex();
if (selected < 0) {
// no selection before, select the first entry
if (!NatCombo.this.useCheckbox) {
select(0);
} else {
getDropdownTable().select(0);
}
event.doit = false;
}
}
}
});
this.dropdownTable.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
boolean selected = e.detail != SWT.CHECK;
boolean isCtrlPressed = (e.stateMask & SWT.MODIFIER_MASK) == SWT.CTRL;
TableItem chosenItem = (TableItem) e.item;
// Given the ability to filter we need to find the item's
// table index which may not match the index in the itemList
int itemTableIndex = NatCombo.this.dropdownTable.indexOf(chosenItem);
// This case handles check actions
if (!selected) {
if (!chosenItem.getChecked()) {
NatCombo.this.selectionStateMap.put(chosenItem.getText(), Boolean.FALSE);
} else {
NatCombo.this.selectionStateMap.put(chosenItem.getText(), Boolean.TRUE);
}
} else if (!NatCombo.this.useCheckbox) {
if (NatCombo.this.multiselect && isCtrlPressed) {
boolean isSelected = NatCombo.this.dropdownTable.isSelected(itemTableIndex);
NatCombo.this.selectionStateMap.put(chosenItem.getText(), isSelected);
if (NatCombo.this.useCheckbox) {
chosenItem.setChecked(isSelected);
}
} else {
// A single item was selected. Clear all previous state
for (String item : NatCombo.this.itemList) {
NatCombo.this.selectionStateMap.put(item, Boolean.FALSE);
}
// Set the state for the selected item
NatCombo.this.selectionStateMap.put(chosenItem.getText(), Boolean.TRUE);
}
}
updateTextControl(false);
}
});
this.dropdownTable.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent event) {
if ((event.keyCode == SWT.CR)
|| (event.keyCode == SWT.KEYPAD_CR)) {
updateTextControl(true);
} else if (event.keyCode == SWT.F2 && NatCombo.this.freeEdit) {
NatCombo.this.text.forceFocus();
hideDropdownControl();
}
}
});
this.dropdownTable.addFocusListener(new FocusListenerWrapper());
FormLayout layout = new FormLayout();
layout.spacing = 0;
layout.marginHeight = 0;
layout.marginWidth = 0;
this.dropdownShell.setLayout(layout);
FormData dropDownLayoutData = new FormData();
dropDownLayoutData.left = new FormAttachment(0);
dropDownLayoutData.right = new FormAttachment(100);
dropDownLayoutData.bottom = new FormAttachment(100);
if (this.showDropdownFilter) {
this.filterBox = new Text(this.dropdownShell, SWT.BORDER);
this.filterBox.setFont(this.cellStyle.getAttributeValue(CellStyleAttributes.FONT));
this.filterBox.setEnabled(true);
this.filterBox.setEditable(true);
this.filterBox.addFocusListener(new FocusListenerWrapper());
this.filterBox.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
if (null != NatCombo.this.dropdownTableViewer && !NatCombo.this.dropdownTable.isDisposed()) {
NatCombo.this.dropdownTableViewer.refresh();
calculateBounds();
setDropdownSelection(getTextAsArray());
}
}
});
FormData data = new FormData();
data.top = new FormAttachment(0);
data.left = new FormAttachment(0);
data.right = new FormAttachment(100);
this.filterBox.setLayoutData(data);
data = new FormData();
dropDownLayoutData.top = new FormAttachment(this.filterBox, 0, SWT.BOTTOM);
this.dropdownTable.setLayoutData(dropDownLayoutData);
ViewerFilter viewerFilter = new ViewerFilter() {
@Override
public boolean select(Viewer viewer, Object parentElement, Object element) {
if (null != element && element instanceof String) {
return ((String) element).toLowerCase().contains(NatCombo.this.filterBox.getText().toLowerCase());
}
return false;
}
};
this.dropdownTableViewer.addFilter(viewerFilter);
} else {
dropDownLayoutData.top = new FormAttachment(0);
this.dropdownTable.setLayoutData(dropDownLayoutData);
}
if (this.itemList != null) {
setItems(this.itemList.toArray(new String[] {}));
}
// apply the listeners that were registered before the creation of the
// dropdown control
applyDropdownListener();
setDropdownSelection(getTextAsArray());
}
/**
* This method will be called if an item of the dropdown control is selected
* via mouse click or pressing enter. It will populate the text control with
* the information gathered out of the selection in the dropdown control and
* hide the dropdown if necessary.
*
* @param hideDropdown
* <code>true</code> if the dropdown should be hidden after
* updating the text control
*/
protected void updateTextControl(boolean hideDropdown) {
this.text.setText(getTransformedTextForSelection());
if (hideDropdown) {
hideDropdownControl();
}
}
/**
* Shows the dropdown of this NatCombo. Will always calculate the size of
* the dropdown regarding the current size of the Text control.
*/
public void showDropdownControl() {
showDropdownControl(false);
}
/**
* Shows the dropdown of this NatCombo. Will always calculate the size of
* the dropdown regarding the current size of the Text control.
*
* @param focusOnText
* <code>true</code> if the focus should be set to the text
* control instead of the dropdown after opening the dropdown.
*/
public void showDropdownControl(boolean focusOnText) {
if (this.dropdownShell == null || this.dropdownShell.isDisposed()) {
createDropdownControl(this.widgetStyle);
}
calculateBounds();
this.dropdownShell.open();
if (focusOnText) {
this.text.forceFocus();
this.text.setSelection(this.text.getText().length());
}
}
/**
* @return The {@link Table} control that is used in the dropdown. Will be
* created if it does not exist yet.
* @since 1.5
*/
protected Table getDropdownTable() {
if (this.dropdownTable == null) {
createDropdownControl(this.widgetStyle);
}
return this.dropdownTable;
}
/**
* Hide the dropdown of this NatCombo.
*/
public void hideDropdownControl() {
if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
this.dropdownShell.setVisible(false);
}
}
/**
* Calculates the number of items that should be showed in the dropdown at
* once. It is needed to calculate the height of the dropdown. If
* maxVisibleItems is configured -1, this method always returns the number
* of items in the list. Otherwise if will return the configured maximum
* number of items to be visible at once or less if there are less than the
* configured maximum.
*
* @return the number of items that should be showed in the dropdown at
* once.
*/
protected int getVisibleItemCount() {
int itemCount = getDropdownTable().getItemCount();
if (itemCount > 0) {
// if maxVisibleItems == -1 show all items at once
// otherwise use the minimum for item count or max visible item
// configuration
int visibleItemCount = itemCount;
if (this.maxVisibleItems > 0) {
visibleItemCount = Math.min(itemCount, this.maxVisibleItems);
}
itemCount = visibleItemCount;
}
return itemCount;
}
/**
* Calculates the size and location of the Shell that represents the
* dropdown control of this NatCombo. Size and location will be calculated
* dependent the position and size of the corresponding Text control and the
* information showed in the dropdown.
*/
protected void calculateBounds() {
if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
Point size = getSize();
// calculate the height by multiplying the number of visible items
// with the item height of items in the list and adding 2*grid line
// width to work around a calculation error regarding the descent of
// the font metrics for the last shown item
// Note: if there are no items to show in the combo, calculate with
// the item count of 3 so an empty combo will open
int listHeight = (getVisibleItemCount() > 0 ? getVisibleItemCount() : 3)
* this.dropdownTable.getItemHeight()
+ this.dropdownTable.getGridLineWidth() * 2;
// since introduced the TableColumn for real full row selection, we
// call pack() to perform autoresize to ensure the width shows the
// whole content
this.dropdownTable.getColumn(0).pack();
int listWidth = Math.max(
this.dropdownTable.computeSize(SWT.DEFAULT, listHeight, true).x, size.x);
Point textPosition = this.text.toDisplay(this.text.getLocation());
// by default the dropdown shell will be created below the cell in
// the table
int dropdownShellStartingY = textPosition.y + this.text.getBounds().height;
int textBottomY = textPosition.y + this.text.getBounds().height + listHeight;
// if the bottom of the drowdown is below the display, render it
// above the cell
if (textBottomY > Display.getCurrent().getBounds().height) {
dropdownShellStartingY = textPosition.y - listHeight;
}
Rectangle parentBounds = getParent().getBounds();
Point parentStart = getParent().toDisplay(parentBounds.x, parentBounds.y);
int parentBottomY = parentStart.y + parentBounds.height - parentBounds.y;
if (getParent().getHorizontalBar() != null && getParent().getHorizontalBar().isVisible()) {
parentBottomY -= getParent().getHorizontalBar().getSize().y;
}
if (dropdownShellStartingY > parentBottomY) {
dropdownShellStartingY = parentBottomY;
}
int filterTextBoxHeight = this.showDropdownFilter ? this.filterBox.computeSize(SWT.DEFAULT, SWT.DEFAULT).y : 0;
Rectangle shellBounds = new Rectangle(
textPosition.x,
dropdownShellStartingY,
listWidth + (this.dropdownTable.getGridLineWidth() * 2),
listHeight + filterTextBoxHeight);
this.dropdownShell.setBounds(shellBounds);
calculateColumnWidth();
}
}
/**
* Calculates and applies the column width to ensure that the column has the
* same width as the table itself, so selection is possible for the whole
* row.
*/
protected void calculateColumnWidth() {
int width = this.dropdownTable.getBounds().width;
// only reduce if a scrollbar is available and visible
if (this.dropdownTable.getVerticalBar() != null
&& this.dropdownTable.getItemCount() > this.maxVisibleItems
&& this.maxVisibleItems > 0) {
width -= this.dropdownTable.getVerticalBar().getSize().x;
}
this.dropdownTable.getColumn(0).setWidth(width);
}
/**
* Returns the zero-relative index of the item which is currently selected
* in the receiver, or -1 if no item is selected.
* <p>
* Note that this only returns useful results if this NatCombo supports
* single selection or only one item is selected.
*
* @return the index of the selected item or -1
*/
public int getSelectionIndex() {
if (this.selectionStateMap != null) {
for (String item : this.selectionStateMap.keySet()) {
if (this.selectionStateMap.get(item)) {
return this.itemList.indexOf(item);
}
}
} else if (!this.text.isDisposed()) {
return this.itemList.indexOf(this.text.getText());
}
return -1;
}
/**
* Returns the zero-relative indices of the items which are currently
* selected in the receiver. The order of the indices is unspecified. The
* array is empty if no items are selected.
* <p>
* Note: This is not the actual structure used by the receiver to maintain
* its selection, so modifying the array will not affect the receiver.
* </p>
*
* @return the array of indices of the selected items
*/
public int[] getSelectionIndices() {
if (this.selectionStateMap != null) {
List<Integer> selectedIndices = new ArrayList<Integer>();
for (String item : this.selectionStateMap.keySet()) {
if (this.selectionStateMap.get(item)) {
selectedIndices.add(this.itemList.indexOf(item));
}
}
int[] indices = new int[selectedIndices.size()];
for (int i = 0; i < selectedIndices.size(); i++) {
indices[i] = selectedIndices.get(i);
}
return indices;
} else {
String[] selectedItems = getTextAsArray();
int[] result = new int[selectedItems.length];
for (int i = 0; i < selectedItems.length; i++) {
result[i] = this.itemList.indexOf(selectedItems[i]);
}
return result;
}
}
/**
* Returns the number of selected items contained in the receiver.
*
* @return the number of selected items
*/
public int getSelectionCount() {
if (this.selectionStateMap != null) {
List<Integer> selectedIndices = new ArrayList<Integer>();
for (String item : this.selectionStateMap.keySet()) {
if (this.selectionStateMap.get(item)) {
selectedIndices.add(this.itemList.indexOf(item));
}
}
return selectedIndices.size();
} else {
return getTextAsArray().length;
}
}
/**
* Returns an array of <code>String</code>s that are currently selected in
* the receiver. The order of the items is unspecified. An empty array
* indicates that no items are selected.
* <p>
* Note: This is not the actual structure used by the receiver to maintain
* its selection, so modifying the array will not affect the receiver.
* </p>
*
* @return an array representing the selection
*/
public String[] getSelection() {
String[] result = getTransformedSelection();
if (result == null
|| (result.length == 0 && !this.text.isDisposed() && this.text.getText().length() > 0)) {
result = getTextAsArray();
}
return result;
}
/**
* Selects the items at the given zero-relative indices in the receiver. The
* current selection is cleared before the new items are selected.
* <p>
* Indices that are out of range and duplicate indices are ignored. If the
* receiver is single-select and multiple indices are specified, then all
* indices are ignored.
* <p>
* The text control of this NatCombo will also be updated with the new
* selected values.
*
* @param items
* the items to select
*/
public void setSelection(String[] items) {
String textValue = ""; //$NON-NLS-1$
if (items != null) {
if (!getDropdownTable().isDisposed()) {
setDropdownSelection(items);
if (this.freeEdit
&& getDropdownTable().getSelectionCount() == 0) {
textValue = getTransformedText(items);
} else {
textValue = getTransformedTextForSelection();
}
} else {
textValue = getTransformedText(items);
}
}
this.text.setText(textValue);
if (this.multiselect) {
this.text.setSelection(textValue.length() - this.multiselectTextSuffix.length());
}
}
/**
* 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
*/
public void select(int index) {
if (!getDropdownTable().isDisposed()) {
getDropdownTable().select(index);
for (int i = 0; i < this.itemList.size(); i++) {
this.selectionStateMap.put(this.itemList.get(i), i == index);
}
this.text.setText(getTransformedTextForSelection());
} else if (index >= 0) {
this.text.setText(this.itemList.get(index));
}
}
/**
* Selects the items at the given zero-relative indices in the receiver. The
* current selection is not cleared before the new items are selected.
* <p>
* If the item at a given index is not selected, it is selected. If the item
* at a given index was already selected, it remains selected. Indices that
* are out of range and duplicate indices are ignored. If the receiver is
* single-select and multiple indices are specified, then all indices are
* ignored.
*
* @param indices
* the array of indices for the items to select
*/
public void select(int[] indices) {
if (!getDropdownTable().isDisposed()) {
getDropdownTable().select(indices);
List<Integer> indicesList = ArrayUtil.asIntegerList(indices);
for (int i = 0; i < this.itemList.size(); i++) {
this.selectionStateMap.put(this.itemList.get(i), indicesList.contains(i));
}
this.text.setText(getTransformedTextForSelection());
} else {
String[] selectedItems = new String[indices.length];
for (int i = 0; i < indices.length; i++) {
if (indices[i] >= 0) {
selectedItems[i] = this.itemList.get(indices[i]);
}
}
this.text.setText(getTransformedText(selectedItems));
}
}
/**
* @since 1.5
*/
protected void applyDropdownListener() {
for (KeyListener l : this.keyListener) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addKeyListener(l);
}
}
for (TraverseListener l : this.traverseListener) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addTraverseListener(l);
}
}
for (MouseListener l : this.mouseListener) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addMouseListener(l);
}
}
for (SelectionListener l : this.selectionListener) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addSelectionListener(l);
}
}
for (ShellListener l : this.shellListener) {
if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
this.dropdownShell.addShellListener(l);
}
}
}
@Override
public void addKeyListener(KeyListener listener) {
if (listener != null) {
if (this.text != null && !this.text.isDisposed()) {
this.text.addKeyListener(listener);
}
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addKeyListener(listener);
}
this.keyListener.add(listener);
}
}
@Override
public void removeKeyListener(KeyListener listener) {
if (this.text != null && !this.text.isDisposed()) {
this.text.removeKeyListener(listener);
}
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.removeKeyListener(listener);
}
this.keyListener.remove(listener);
}
@Override
public void addTraverseListener(TraverseListener listener) {
if (listener != null) {
if (this.text != null && !this.text.isDisposed()) {
this.text.addTraverseListener(listener);
}
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addTraverseListener(listener);
}
this.traverseListener.add(listener);
}
}
@Override
public void removeTraverseListener(TraverseListener listener) {
if (this.text != null && !this.text.isDisposed()) {
this.text.removeTraverseListener(listener);
}
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.removeTraverseListener(listener);
}
this.traverseListener.remove(listener);
}
@Override
public void addMouseListener(MouseListener listener) {
// only add the mouse listener to the dropdown, as clicking in the text
// control should not trigger anything else than it is handled by the
// text control itself.
if (listener != null) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addMouseListener(listener);
}
this.mouseListener.add(listener);
}
}
@Override
public void removeMouseListener(MouseListener listener) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.removeMouseListener(listener);
}
this.mouseListener.remove(listener);
}
@Override
public void notifyListeners(int eventType, Event event) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.notifyListeners(eventType, event);
}
}
public void addSelectionListener(SelectionListener listener) {
if (listener != null) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.addSelectionListener(listener);
}
this.selectionListener.add(listener);
}
}
public void removeSelectionListener(SelectionListener listener) {
if (this.dropdownTable != null && !this.dropdownTable.isDisposed()) {
this.dropdownTable.removeSelectionListener(listener);
}
this.selectionListener.remove(listener);
}
public void addShellListener(ShellListener listener) {
if (listener != null) {
if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
this.dropdownShell.addShellListener(listener);
}
this.shellListener.add(listener);
}
}
public void removeShellListener(ShellListener listener) {
if (this.dropdownShell != null && !this.dropdownShell.isDisposed()) {
this.dropdownShell.removeShellListener(listener);
}
this.shellListener.remove(listener);
}
public void addTextControlListener(ControlListener listener) {
if (listener != null && this.text != null && !this.text.isDisposed()) {
this.text.addControlListener(listener);
}
}
public void removeTextControlListener(ControlListener listener) {
if (this.text != null && !this.text.isDisposed()) {
this.text.removeControlListener(listener);
}
}
@Override
public boolean isFocusControl() {
return this.hasFocus;
}
@Override
public boolean forceFocus() {
return this.text.forceFocus();
}
@Override
public void addFocusListener(FocusListener listener) {
if (listener != null) {
this.focusListener.add(listener);
}
}
@Override
public void removeFocusListener(final FocusListener listener) {
// The FocusListenerWrapper is executing the focusLost event
// in a separate thread with 100ms delay to ensure that the NatCombo
// lost focus. This is necessary because the NatCombo is a combination
// of a text field and a table as dropdown which do not share the
// same focus by default.
this.focusListener.remove(listener);
}
/**
* Transforms the selection in the Table control dropdown into a String[].
* Doing this is necessary to provide a SWT List like interface regarding
* selections for the NatCombo.
*
* @return Array containing all selected TableItem text attributes
*/
protected String[] getTransformedSelection() {
List<String> selectedItems = new ArrayList<String>();
for (String item : this.selectionStateMap.keySet()) {
Boolean isSelected = this.selectionStateMap.get(item);
if (isSelected != null && isSelected) {
selectedItems.add(item);
}
}
return selectedItems.toArray(new String[selectedItems.size()]);
}
/**
* Transforms the given String array whose contents represents selected
* items to a selection that can be handled by the underlying Table control
* in the dropdown.
*
* @param selection
* The Strings that represent the selected items
*/
protected void setDropdownSelection(String[] selection) {
java.util.List<String> selectionList = Arrays.asList(selection);
java.util.List<TableItem> selectedItems = new ArrayList<TableItem>();
for (TableItem item : getDropdownTable().getItems()) {
if (selectionList.contains(EditConstants.SELECT_ALL_ITEMS_VALUE)
|| selectionList.contains(item.getText())) {
this.selectionStateMap.put(item.getText(), Boolean.TRUE);
if (this.useCheckbox) {
item.setChecked(true);
} else {
selectedItems.add(item);
}
} else {
this.selectionStateMap.put(item.getText(), Boolean.FALSE);
}
}
getDropdownTable().setSelection(selectedItems.toArray(new TableItem[] {}));
}
/**
* Will transform the text for the Text control of this NatCombo to an array
* of Strings. This is necessary for the multiselect feature.
*
* <p>
* Note that by default the multiselect String is specified to show with
* enclosing [] brackets and values separated by ", ". If you need to change
* this you need to set the corresponding values in this NatCombo.
*
* @return The text for the Text control of this NatCombo converted to an
* array of Strings.
*/
protected String[] getTextAsArray() {
if (!this.text.isDisposed()) {
String transform = this.text.getText();
if (transform.length() > 0) {
if (this.multiselect) {
// for multiselect the String is defined by default in
// format [a, b, c]
// the prefix and suffix for multiselect String
// representation need to be removed
// in free edit mode we need to check if the format is used
int prefixLength = this.multiselectTextPrefix.length();
int suffixLength = this.multiselectTextSuffix.length();
if (this.freeEdit) {
if (!transform.startsWith(this.multiselectTextPrefix)) {
prefixLength = 0;
}
if (!transform.endsWith(this.multiselectTextSuffix)) {
suffixLength = 0;
}
}
transform = transform.substring(prefixLength, transform.length() - suffixLength);
// if the transform value length is still > 0, try to split
if (transform.length() > 0) {
return transform.split(this.multiselectValueSeparator);
}
}
return new String[] { transform };
}
}
return new String[] {};
}
/**
* Transforms the selection of the dropdown to a text representation that
* can be added to the text control of this combo.
*
* <p>
* Note that by default the multiselect String is specified to show with
* enclosing [] brackets and values separated by ", ". If you need to change
* this you need to set the corresponding values in this NatCombo.
*
* @return String representation for the selection within the combo.
*/
protected String getTransformedTextForSelection() {
String result = ""; //$NON-NLS-1$
String[] selection = getTransformedSelection();
if (selection != null) {
result = getTransformedText(selection);
}
return result;
}
/**
* Transforms the given array of Strings to a text representation that can
* be added to the text control of this combo.
* <p>
* If this NatCombo is only configured to support single selection, than
* only the first value in the array will be processed. Otherwise the result
* will be processed by concatenating the values.
* <p>
* Note that by default the multiselect String is specified to show with
* enclosing [] brackets and values separated by ", ". If you need to change
* this you need to set the corresponding values in this NatCombo.
*
* @param values
* The values to build the text representation from.
* @return String representation for the selection within the combo.
*/
protected String getTransformedText(String[] values) {
String result = ""; //$NON-NLS-1$
if (this.multiselect) {
for (int i = 0; i < values.length; i++) {
String selection = values[i];
result += selection;
if ((i + 1) < values.length) {
result += this.multiselectValueSeparator;
}
}
result = this.multiselectTextPrefix + result + this.multiselectTextSuffix;
} else if (values.length > 0) {
result = values[0];
}
return result;
}
/**
* @param multiselectValueSeparator
* String that should be used to separate values in the String
* representation showed in the text control if multiselect is
* supported. <code>null</code> to use the default value
* separator.
* @see NatCombo#DEFAULT_MULTI_SELECT_VALUE_SEPARATOR
*/
public void setMultiselectValueSeparator(String multiselectValueSeparator) {
if (multiselectValueSeparator == null) {
this.multiselectValueSeparator = DEFAULT_MULTI_SELECT_VALUE_SEPARATOR;
} else {
this.multiselectValueSeparator = multiselectValueSeparator;
}
}
/**
* Set the prefix and suffix that will parenthesize the text that is created
* out of the selected values if this NatCombo supports multiselection.
*
* @param multiselectTextPrefix
* String that should be used to prefix the generated String
* representation showed in the text control if multiselect is
* supported. <code>null</code> to use the default prefix.
* @param multiselectTextSuffix
* String that should be used to suffix the generated String
* representation showed in the text control if multiselect is
* supported. <code>null</code> to use the default suffix.
* @see NatCombo#DEFAULT_MULTI_SELECT_PREFIX
* @see NatCombo#DEFAULT_MULTI_SELECT_SUFFIX
*/
public void setMultiselectTextBracket(String multiselectTextPrefix, String multiselectTextSuffix) {
if (multiselectTextPrefix == null) {
this.multiselectTextPrefix = DEFAULT_MULTI_SELECT_PREFIX;
} else {
this.multiselectTextPrefix = multiselectTextPrefix;
}
if (multiselectTextSuffix == null) {
this.multiselectTextSuffix = DEFAULT_MULTI_SELECT_SUFFIX;
} else {
this.multiselectTextSuffix = multiselectTextSuffix;
}
}
/**
* FocusListener that is used to ensure that the Text control and the
* dropdown table control are sharing the same focus. If either of both
* controls looses focus, the local focus flag is set to false and a delayed
* background thread for focus lost is started. If the other control gains
* focus, the local focus flag is set to true which skips the execution of
* the delayed background thread. This means the NatCombo hasn't lost focus.
*
* @since 1.4
*/
public class FocusListenerWrapper implements FocusListener {
@Override
public void focusLost(final FocusEvent e) {
NatCombo.this.hasFocus = false;
Display.getCurrent().timerExec(100, new Runnable() {
@Override
public void run() {
if (!NatCombo.this.hasFocus) {
List<FocusListener> copy = new ArrayList<FocusListener>(NatCombo.this.focusListener);
for (FocusListener f : copy) {
f.focusLost(e);
}
}
}
});
}
@Override
public void focusGained(FocusEvent e) {
NatCombo.this.hasFocus = true;
for (FocusListener f : NatCombo.this.focusListener) {
f.focusGained(e);
}
}
}
}