| /******************************************************************************* |
| * Copyright (c) 2008 Oracle. 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: |
| * Oracle - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.jpt.ui.internal.swt; |
| |
| import java.util.EventListener; |
| import java.util.EventObject; |
| import java.util.Iterator; |
| import java.util.ListIterator; |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.jpt.ui.internal.listeners.SWTListChangeListenerWrapper; |
| import org.eclipse.jpt.ui.internal.listeners.SWTPropertyChangeListenerWrapper; |
| import org.eclipse.jpt.utility.internal.CollectionTools; |
| import org.eclipse.jpt.utility.internal.ListenerList; |
| import org.eclipse.jpt.utility.internal.StringConverter; |
| import org.eclipse.jpt.utility.internal.StringTools; |
| import org.eclipse.jpt.utility.model.event.CollectionChangeEvent; |
| import org.eclipse.jpt.utility.model.event.ListChangeEvent; |
| import org.eclipse.jpt.utility.model.event.PropertyChangeEvent; |
| import org.eclipse.jpt.utility.model.listener.ListChangeListener; |
| import org.eclipse.jpt.utility.model.listener.PropertyChangeListener; |
| import org.eclipse.jpt.utility.model.value.ListValueModel; |
| import org.eclipse.jpt.utility.model.value.PropertyValueModel; |
| import org.eclipse.jpt.utility.model.value.WritablePropertyValueModel; |
| import org.eclipse.swt.events.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| 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; |
| |
| /** |
| * This adapter provides a more object-oriented interface to the items and |
| * selected item in a combo. |
| * <p> |
| * <b>listHolder</b> contains the items in the combo.<br> |
| * <b>selectedItemHolder</b> contains the items in 'listHolder' that are |
| * selected in the combo. |
| * |
| * @param <E> The type of the items in <b>listHolder</b> |
| * @see ComboModelAdapter |
| * @see CComboModelAdapter |
| * |
| * @version 2.0 |
| * @since 2.0 |
| */ |
| @SuppressWarnings("nls") |
| public abstract class AbstractComboModelAdapter<E> { |
| |
| // ********** model ********** |
| /** |
| * A value model on the underlying model list. |
| */ |
| protected final ListValueModel<E> listHolder; |
| |
| /** |
| * A listener that allows us to synchronize the combo's contents with |
| * the model list. |
| */ |
| protected final ListChangeListener listChangeListener; |
| |
| /** |
| * A value model on the underlying model selection. |
| */ |
| protected final WritablePropertyValueModel<E> selectedItemHolder; |
| |
| /** |
| * A listener that allows us to synchronize the combo's selection with the |
| * model selection. |
| */ |
| protected final PropertyChangeListener selectedItemChangeListener; |
| |
| /** |
| * A converter that converts items in the model list |
| * to strings that can be put in the combo. |
| */ |
| protected StringConverter<E> stringConverter; |
| |
| // ********** UI ********** |
| /** |
| * The combo we keep synchronized with the model list. |
| */ |
| protected final ComboHolder comboHolder; |
| |
| /** |
| * A listener that allows us to synchronize our selection list holder |
| * with the combo's text. |
| */ |
| protected ModifyListener comboModifyListener; |
| |
| /** |
| * A listener that allows us to synchronize our selection list holder |
| * with the combo's selection. |
| */ |
| protected SelectionListener comboSelectionListener; |
| |
| /** |
| * Clients that are interested in selection change events. |
| */ |
| @SuppressWarnings("unchecked") |
| protected final ListenerList<SelectionChangeListener> selectionChangeListenerList; |
| |
| /** |
| * Clients that are interested in double click events. |
| */ |
| @SuppressWarnings("unchecked") |
| protected final ListenerList<DoubleClickListener> doubleClickListenerList; |
| |
| /** |
| * A listener that allows us to stop listening to stuff when the combo |
| * is disposed. |
| */ |
| protected final DisposeListener comboDisposeListener; |
| |
| |
| // ********** constructors ********** |
| |
| /** |
| * Constructor - the list holder, selections holder, combo, and |
| * string converter are required. |
| */ |
| protected AbstractComboModelAdapter( |
| ListValueModel<E> listHolder, |
| WritablePropertyValueModel<E> selectedItemHolder, |
| ComboHolder comboHolder, |
| StringConverter<E> stringConverter) |
| { |
| super(); |
| |
| Assert.isNotNull(listHolder, "The holder of the items"); |
| Assert.isNotNull(selectedItemHolder, "The holder of the selected item cannot be null"); |
| Assert.isNotNull(comboHolder, "The holder of the combo widget cannot be null"); |
| Assert.isNotNull(stringConverter, "The string converter cannot be null"); |
| |
| this.listHolder = listHolder; |
| this.selectedItemHolder = selectedItemHolder; |
| this.comboHolder = comboHolder; |
| this.stringConverter = stringConverter; |
| |
| this.listChangeListener = this.buildListChangeListener(); |
| this.listHolder.addListChangeListener(ListValueModel.LIST_VALUES, this.listChangeListener); |
| |
| this.selectedItemChangeListener = this.buildSelectedItemChangeListener(); |
| this.selectedItemHolder.addPropertyChangeListener(PropertyValueModel.VALUE, this.selectedItemChangeListener); |
| |
| if (this.comboHolder.isEditable()) { |
| this.comboModifyListener = this.buildComboModifyListener(); |
| this.comboHolder.addModifyListener(this.comboModifyListener); |
| } |
| else { |
| this.comboSelectionListener = this.buildComboSelectionListener(); |
| this.comboHolder.addSelectionListener(this.comboSelectionListener); |
| } |
| |
| this.selectionChangeListenerList = this.buildSelectionChangeListenerList(); |
| this.doubleClickListenerList = this.buildDoubleClickListenerList(); |
| |
| this.comboDisposeListener = this.buildComboDisposeListener(); |
| this.comboHolder.addDisposeListener(this.comboDisposeListener); |
| |
| this.synchronizeCombo(); |
| } |
| |
| |
| // ********** initialization ********** |
| |
| protected ListChangeListener buildListChangeListener() { |
| return new SWTListChangeListenerWrapper(this.buildListChangeListener_()); |
| } |
| |
| protected ListChangeListener buildListChangeListener_() { |
| return new ListChangeListener() { |
| public void itemsAdded(ListChangeEvent event) { |
| AbstractComboModelAdapter.this.listItemsAdded(event); |
| } |
| public void itemsRemoved(ListChangeEvent event) { |
| AbstractComboModelAdapter.this.listItemsRemoved(event); |
| } |
| public void itemsMoved(ListChangeEvent event) { |
| AbstractComboModelAdapter.this.listItemsMoved(event); |
| } |
| public void itemsReplaced(ListChangeEvent event) { |
| AbstractComboModelAdapter.this.listItemsReplaced(event); |
| } |
| public void listCleared(ListChangeEvent event) { |
| AbstractComboModelAdapter.this.listCleared(event); |
| } |
| public void listChanged(ListChangeEvent event) { |
| AbstractComboModelAdapter.this.listChanged(event); |
| } |
| @Override |
| public String toString() { |
| return "list listener"; |
| } |
| }; |
| } |
| |
| protected PropertyChangeListener buildSelectedItemChangeListener() { |
| return new SWTPropertyChangeListenerWrapper(this.buildSelectedItemChangeListener_()); |
| } |
| |
| protected PropertyChangeListener buildSelectedItemChangeListener_() { |
| return new PropertyChangeListener() { |
| public void propertyChanged(PropertyChangeEvent e) { |
| AbstractComboModelAdapter.this.selectedItemChanged(e); |
| } |
| }; |
| } |
| |
| protected ModifyListener buildComboModifyListener() { |
| return new ModifyListener() { |
| public void modifyText(ModifyEvent event) { |
| AbstractComboModelAdapter.this.comboSelectionChanged(event); |
| } |
| |
| @Override |
| public String toString() { |
| return "combo modify listener"; |
| } |
| }; |
| } |
| |
| protected SelectionListener buildComboSelectionListener() { |
| return new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent event) { |
| AbstractComboModelAdapter.this.comboSelectionChanged(event); |
| } |
| |
| @Override |
| public String toString() { |
| return "combo modify listener"; |
| } |
| }; |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected ListenerList<DoubleClickListener> buildDoubleClickListenerList() { |
| return new ListenerList<DoubleClickListener>(DoubleClickListener.class); |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected ListenerList<SelectionChangeListener> buildSelectionChangeListenerList() { |
| return new ListenerList<SelectionChangeListener>(SelectionChangeListener.class); |
| } |
| |
| protected DisposeListener buildComboDisposeListener() { |
| return new DisposeListener() { |
| public void widgetDisposed(DisposeEvent event) { |
| AbstractComboModelAdapter.this.comboDisposed(event); |
| } |
| |
| @Override |
| public String toString() { |
| return "combo dispose listener"; |
| } |
| }; |
| } |
| |
| protected void synchronizeCombo() { |
| this.synchronizeComboItems(); |
| this.synchronizeComboSelection(); |
| } |
| |
| |
| // ********** string converter ********** |
| |
| public void setStringConverter(StringConverter<E> stringConverter) { |
| Assert.isNotNull(stringConverter, "The StringConverter cannot be null"); |
| this.stringConverter = stringConverter; |
| this.synchronizeCombo(); |
| } |
| |
| |
| // ********** list ********** |
| |
| /** |
| * Use the string converter to convert the specified item to a |
| * string that can be added to the combo. |
| */ |
| protected String convert(E item) { |
| return this.stringConverter.convertToString(item); |
| } |
| |
| /** |
| * Brute force synchronization of combo with the model list. |
| */ |
| protected void synchronizeComboItems() { |
| if (this.comboHolder.isDisposed()) { |
| return; |
| } |
| int len = this.listHolder.size(); |
| String[] items = new String[len]; |
| for (int index = 0; index < len; index++) { |
| items[index] = this.convert(this.listHolder.get(index)); |
| } |
| try { |
| this.comboHolder.setPopulating(true); |
| this.comboHolder.setItems(items); |
| } |
| finally { |
| this.comboHolder.setPopulating(false); |
| } |
| } |
| |
| /** |
| * The model has changed - synchronize the combo. |
| */ |
| protected void listItemsAdded(ListChangeEvent event) { |
| if (this.comboHolder.isDisposed()) { |
| return; |
| } |
| |
| int count = this.comboHolder.getItemCount(); |
| int index = event.getIndex(); |
| |
| for (ListIterator<E> stream = this.items(event); stream.hasNext(); ) { |
| this.comboHolder.add(this.convert(stream.next()), index++); |
| } |
| |
| // When the combo is populated, it's possible the selection was already |
| // set but no items was found, resync the selected item |
| synchronizeComboSelection(); |
| } |
| |
| /** |
| * The model has changed - synchronize the combo. |
| */ |
| protected void listItemsRemoved(ListChangeEvent event) { |
| if (this.comboHolder.isDisposed()) { |
| return; |
| } |
| this.comboHolder.remove(event.getIndex(), event.getIndex() + event.itemsSize() - 1); |
| this.synchronizeComboSelection(); |
| } |
| |
| /** |
| * The model has changed - synchronize the combo. |
| */ |
| protected void listItemsMoved(ListChangeEvent event) { |
| if (this.comboHolder.isDisposed()) { |
| return; |
| } |
| int target = event.getTargetIndex(); |
| int source = event.getSourceIndex(); |
| int len = event.getMoveLength(); |
| int loStart = Math.min(target, source); |
| int hiStart = Math.max(target, source); |
| // make a copy of the affected items... |
| String[] subArray = CollectionTools.subArray(this.comboHolder.getItems(), loStart, hiStart + len - loStart); |
| // ...move them around... |
| subArray = CollectionTools.move(subArray, target - loStart, source - loStart, len); |
| // ...and then put them back |
| for (int index = 0; index < subArray.length; index++) { |
| this.comboHolder.setItem(loStart + index, subArray[index]); |
| } |
| } |
| |
| /** |
| * The model has changed - synchronize the combo. |
| */ |
| protected void listItemsReplaced(ListChangeEvent event) { |
| if (this.comboHolder.isDisposed()) { |
| return; |
| } |
| int index = event.getIndex(); |
| for (ListIterator<E> stream = this.items(event); stream.hasNext(); ) { |
| this.comboHolder.setItem(index++, this.convert(stream.next())); |
| } |
| } |
| |
| /** |
| * The model has changed - synchronize the combo. |
| */ |
| protected void listCleared(ListChangeEvent event) { |
| if (this.comboHolder.isDisposed()) { |
| return; |
| } |
| this.comboHolder.removeAll(); |
| } |
| |
| /** |
| * The model has changed - synchronize the combo. |
| */ |
| protected void listChanged(ListChangeEvent event) { |
| this.synchronizeComboItems(); |
| } |
| |
| // minimized unchecked code |
| @SuppressWarnings("unchecked") |
| protected ListIterator<E> items(ListChangeEvent event) { |
| return ((ListIterator<E>) event.items()); |
| } |
| |
| |
| // ********** selected items ********** |
| |
| protected int indexOf(E item) { |
| int length = this.listHolder.size(); |
| for (int index = 0; index < length; index++) { |
| if (valuesAreEqual(this.listHolder.get(index), item)) { |
| return index; |
| } |
| } |
| return -1; |
| } |
| |
| protected void synchronizeComboSelection() { |
| if (this.comboHolder.isDisposed() || this.comboHolder.isPopulating()) { |
| return; |
| } |
| |
| E selectedValue = this.selectedItemHolder.getValue(); |
| this.comboHolder.setPopulating(true); |
| |
| try { |
| this.comboHolder.deselectAll(); |
| String selectedItem = this.convert(selectedValue); |
| if (selectedItem == null) { |
| selectedItem = ""; |
| } |
| this.comboHolder.setText(selectedItem); |
| this.notifyListeners(selectedValue); |
| } |
| finally { |
| this.comboHolder.setPopulating(false); |
| } |
| } |
| |
| protected void selectedItemChanged(PropertyChangeEvent event) { |
| this.synchronizeComboSelection(); |
| } |
| |
| // minimized unchecked code |
| @SuppressWarnings("unchecked") |
| protected Iterator<E> items(CollectionChangeEvent event) { |
| return ((Iterator<E>) event.items()); |
| } |
| |
| |
| /** |
| * Return whether the values are equal, with the appropriate null checks. |
| * Convenience method for checking whether an attribute value has changed. |
| */ |
| protected final boolean valuesAreEqual(Object value1, Object value2) { |
| if ((value1 == null) && (value2 == null)) { |
| return true; // both are null |
| } |
| if ((value1 == null) || (value2 == null)) { |
| return false; // one is null but the other is not |
| } |
| return value1.equals(value2); |
| } |
| |
| // ********** combo events ********** |
| |
| protected void comboSelectionChanged(SelectionEvent event) { |
| this.selectionChanged(); |
| } |
| |
| protected void comboSelectionChanged(ModifyEvent event) { |
| this.selectionChanged(); |
| } |
| |
| protected void selectionChanged() { |
| if (!this.comboHolder.isPopulating()) { |
| E selectedItem = this.selectedItem(); |
| this.comboHolder.setPopulating(true); |
| try { |
| this.selectedItemHolder.setValue(selectedItem); |
| this.notifyListeners(selectedItem); |
| } |
| finally { |
| this.comboHolder.setPopulating(false); |
| } |
| } |
| } |
| |
| private void notifyListeners(E selectedItem) { |
| if (this.selectionChangeListenerList.size() > 0) { |
| SelectionChangeEvent<E> scEvent = new SelectionChangeEvent<E>(this, selectedItem); |
| for (SelectionChangeListener<E> selectionChangeListener : this.selectionChangeListenerList.getListeners()) { |
| selectionChangeListener.selectionChanged(scEvent); |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected E selectedItem() { |
| if (this.comboHolder.isDisposed()) { |
| return null; |
| } |
| |
| if (this.comboHolder.isEditable()) { |
| String text = this.comboHolder.getText(); |
| |
| if (text.length() == 0) { |
| return null; |
| } |
| |
| for (int index = this.listHolder.size(); --index >= 0; ) { |
| E item = this.listHolder.get(index); |
| String value = this.convert(item); |
| if (valuesAreEqual(text, value)) { |
| return item; |
| } |
| } |
| |
| // TODO: Find a way to prevent this type cast (it'll work if E is |
| // String but it won't work if E is something else), maybe use a |
| // BidiStringConverter instead of StringConverter |
| try { |
| return (E) text; |
| } |
| catch (ClassCastException e) { |
| return null; |
| } |
| } |
| |
| int index = this.comboHolder.getSelectionIndex(); |
| |
| if (index == -1) { |
| return null; |
| } |
| |
| return this.listHolder.get(index); |
| } |
| |
| protected void comboDoubleClicked(SelectionEvent event) { |
| if (this.comboHolder.isDisposed()) { |
| return; |
| } |
| if (this.doubleClickListenerList.size() > 0) { |
| // there should be only a single item selected during a double-click(?) |
| E selection = this.listHolder.get(this.comboHolder.getSelectionIndex()); |
| DoubleClickEvent<E> dcEvent = new DoubleClickEvent<E>(this, selection); |
| for (DoubleClickListener<E> doubleClickListener : this.doubleClickListenerList.getListeners()) { |
| doubleClickListener.doubleClick(dcEvent); |
| } |
| } |
| } |
| |
| |
| // ********** dispose ********** |
| |
| protected void comboDisposed(DisposeEvent event) { |
| // the combo is not yet "disposed" when we receive this event |
| // so we can still remove our listeners |
| this.comboHolder.removeDisposeListener(this.comboDisposeListener); |
| if (this.comboHolder.isEditable()) { |
| this.comboHolder.removeModifyListener(this.comboModifyListener); |
| } |
| else { |
| this.comboHolder.removeSelectionListener(this.comboSelectionListener); |
| } |
| this.selectedItemHolder.removePropertyChangeListener(PropertyValueModel.VALUE, this.selectedItemChangeListener); |
| this.listHolder.removeListChangeListener(ListValueModel.LIST_VALUES, this.listChangeListener); |
| } |
| |
| |
| // ********** standard methods ********** |
| |
| @Override |
| public String toString() { |
| return StringTools.buildToStringFor(this, this.listHolder); |
| } |
| |
| |
| // ********** double click support ********** |
| |
| public void addDoubleClickListener(DoubleClickListener<E> listener) { |
| this.doubleClickListenerList.add(listener); |
| } |
| |
| public void removeDoubleClickListener(DoubleClickListener<E> listener) { |
| this.doubleClickListenerList.remove(listener); |
| } |
| |
| public interface DoubleClickListener<E> extends EventListener { |
| void doubleClick(DoubleClickEvent<E> event); |
| } |
| |
| public static class DoubleClickEvent<E> extends EventObject { |
| private final E selection; |
| private static final long serialVersionUID = 1L; |
| |
| protected DoubleClickEvent(AbstractComboModelAdapter<E> source, E selection) { |
| super(source); |
| if (selection == null) { |
| throw new NullPointerException(); |
| } |
| this.selection = selection; |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public AbstractComboModelAdapter<E> getSource() { |
| return (AbstractComboModelAdapter<E>) super.getSource(); |
| } |
| |
| public E selection() { |
| return this.selection; |
| } |
| } |
| |
| |
| // ********** selection support ********** |
| |
| public void addSelectionChangeListener(SelectionChangeListener<E> listener) { |
| this.selectionChangeListenerList.add(listener); |
| } |
| |
| public void removeSelectionChangeListener(SelectionChangeListener<E> listener) { |
| this.selectionChangeListenerList.remove(listener); |
| } |
| |
| public interface SelectionChangeListener<E> extends EventListener { |
| void selectionChanged(SelectionChangeEvent<E> event); |
| } |
| |
| public static class SelectionChangeEvent<E> extends EventObject { |
| private final E selectedItem; |
| private static final long serialVersionUID = 1L; |
| |
| protected SelectionChangeEvent(AbstractComboModelAdapter<E> source, E selectedItem) { |
| super(source); |
| this.selectedItem = selectedItem; |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public AbstractComboModelAdapter<E> getSource() { |
| return (AbstractComboModelAdapter<E>) super.getSource(); |
| } |
| |
| public E selectedItem() { |
| return this.selectedItem; |
| } |
| } |
| |
| // ********** Internal member ********** |
| |
| /** |
| * This holder is required for supporting <code>Combo</code> and |
| * <code>CCombo</code> transparently. |
| */ |
| protected static interface ComboHolder { |
| void add(String item, int index); |
| void addDisposeListener(DisposeListener disposeListener); |
| void addModifyListener(ModifyListener modifyListener); |
| void addSelectionListener(SelectionListener selectionListener); |
| void deselectAll(); |
| int getItemCount(); |
| String[] getItems(); |
| int getSelectionIndex(); |
| String getText(); |
| boolean isDisposed(); |
| boolean isEditable(); |
| boolean isPopulating(); |
| void removeDisposeListener(DisposeListener disposeListener); |
| void removeModifyListener(ModifyListener modifyListener); |
| void removeSelectionListener(SelectionListener selectionListener); |
| void setItem(int index, String item); |
| void setItems(String[] items); |
| void setPopulating(boolean populating); |
| void setText(String item); |
| void remove(int start, int end); |
| void removeAll(); |
| } |
| } |