blob: ea5c355ecb0a9e13e5e461a681f83a6f0f5195bb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2013 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0, which accompanies this distribution
* and is available at https://www.eclipse.org/legal/epl-2.0/.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.common.ui.internal.swt.bindings;
import java.util.ArrayList;
import java.util.Arrays;
import org.eclipse.jpt.common.ui.internal.swt.events.SelectionAdapter;
import org.eclipse.jpt.common.ui.internal.swt.listeners.SWTListenerTools;
import org.eclipse.jpt.common.utility.internal.ArrayTools;
import org.eclipse.jpt.common.utility.internal.ObjectTools;
import org.eclipse.jpt.common.utility.internal.collection.CollectionTools;
import org.eclipse.jpt.common.utility.model.event.CollectionAddEvent;
import org.eclipse.jpt.common.utility.model.event.CollectionChangeEvent;
import org.eclipse.jpt.common.utility.model.event.CollectionClearEvent;
import org.eclipse.jpt.common.utility.model.event.CollectionRemoveEvent;
import org.eclipse.jpt.common.utility.model.listener.CollectionChangeAdapter;
import org.eclipse.jpt.common.utility.model.listener.CollectionChangeListener;
import org.eclipse.jpt.common.utility.model.value.CollectionValueModel;
import org.eclipse.jpt.common.utility.model.value.ModifiableCollectionValueModel;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.List;
/**
* This binding can be used to keep a list box's selection
* synchronized with a model.
* <p>
* <strong>NB:</strong> This binding is bi-directional. As a result, we modify
* our {@link #selectedItems cached list} <em>only</em> via
* the {@link #selectedItemsModel model collection} change events;
* and we {@link #listBoxSelectionChanged() simply pass through} the list box
* selection events (by calling
* {@link ModifiableCollectionValueModel#setValues(Iterable)},
* which will loop back to us as a collection change event).
* <p>
* <strong>NB2:</strong> Changes to the underlying list model can imply changes
* to the selection collection model. But these changes occur
* <em>asynchronously</em> (as do their resulting events).
* If the models are designed correctly, the selection model will be modified,
* if necessary, whenever the underlying list model changes
* (typically when elements are removed from the underlying list model).
* But <em>all</em> changes to the underlying list model can cause changes to
* the <em>indices</em> of the selected items; so all changes to the underlying
* list model result in calls to {@link #listChanged()}.
* Unfortunately, since the <em>selection</em> change event might not have
* arrived yet, the selection item list can contain elements that are not in
* the underlying list. So we must gracefully handle missing elements, even
* though this may hide coding errors (i.e. something that should <em>not</em>
* happen and should trigger an exception). Likewise, we can receive
* <em>selection</em> change events before the underlying list model is updated,
* resulting, again, in temporarily invalid state (which will be rectified once
* the underlying list model is updated and the binding calls
* {@link #listChanged()}).
*
* @see ModifiableCollectionValueModel
* @see List
* @see SWTBindingTools
*/
final class ListBoxSelectionBinding<E>
implements ListWidgetModelBinding.SelectionBinding
{
// ***** model
/**
* The underlying list (maintained by {@link ListWidgetModelBinding}).
*/
private final ArrayList<E> list;
/**
* A modifiable value model on the underlying model selections.
*/
private final ModifiableCollectionValueModel<E> selectedItemsModel;
/**
* Cache of model selections.
*/
private final ArrayList<E> selectedItems = new ArrayList<E>();
/**
* A listener that allows us to synchronize the list box's selection with
* the model selections.
*/
private final CollectionChangeListener selectedItemsListener;
// ***** UI
/**
* The list box whose selection we keep synchronized
* with the model selections.
*/
private final List listBox;
/**
* A listener that allows us to synchronize our selected items model
* with the list box's selection.
*/
private final SelectionListener listBoxSelectionListener;
/**
* Constructor - all parameters are required.
*/
ListBoxSelectionBinding(
ArrayList<E> list,
ModifiableCollectionValueModel<E> selectedItemsModel,
List listBox
) {
super();
if ((list == null) || (selectedItemsModel == null) || (listBox == null)) {
throw new NullPointerException();
}
this.list = list;
this.selectedItemsModel = selectedItemsModel;
this.listBox = listBox;
this.selectedItemsListener = this.buildSelectedItemsListener();
this.selectedItemsModel.addCollectionChangeListener(CollectionValueModel.VALUES, this.selectedItemsListener);
this.listBoxSelectionListener = this.buildListBoxSelectionListener();
this.listBox.addSelectionListener(this.listBoxSelectionListener);
this.selectedItems.ensureCapacity(this.selectedItemsModel.size());
CollectionTools.addAll(this.selectedItems, this.selectedItemsModel);
}
// ********** initialization **********
private CollectionChangeListener buildSelectedItemsListener() {
return SWTListenerTools.wrap(new SelectedItemsListener(), this.listBox);
}
/* CU private */ class SelectedItemsListener
extends CollectionChangeAdapter
{
@Override
public void itemsAdded(CollectionAddEvent event) {
ListBoxSelectionBinding.this.selectedItemsAdded(event);
}
@Override
public void itemsRemoved(CollectionRemoveEvent event) {
ListBoxSelectionBinding.this.selectedItemsRemoved(event);
}
@Override
public void collectionCleared(CollectionClearEvent event) {
ListBoxSelectionBinding.this.selectedItemsCleared();
}
@Override
public void collectionChanged(CollectionChangeEvent event) {
ListBoxSelectionBinding.this.selectedItemsChanged(event);
}
}
private SelectionListener buildListBoxSelectionListener() {
return new ListBoxSelectionListener();
}
/* CU private */ class ListBoxSelectionListener
extends SelectionAdapter
{
@Override
public void widgetSelected(SelectionEvent event) {
ListBoxSelectionBinding.this.listBoxSelectionChanged();
}
@Override
public void widgetDefaultSelected(SelectionEvent event) {
ListBoxSelectionBinding.this.listBoxDoubleClicked();
}
}
// ********** ListWidgetModelBinding.SelectionBinding implementation **********
/**
* <strong>NB:</strong> The elements in the selection model may be out of
* sync with the underlying list model. (See the class comment.)
* <p>
* Modifying the list box's selected items programmatically does not
* trigger a {@link SelectionEvent}.
* <p>
* Pre-condition: The list-box is not disposed.
*/
public void listChanged() {
this.setListSelection();
}
private void setListSelection() {
int selectedItemsSize = this.selectedItems.size();
int[] select = ArrayTools.EMPTY_INT_ARRAY;
if (selectedItemsSize > 0) {
select = new int[selectedItemsSize];
int i = 0;
for (E item : this.selectedItems) {
select[i++] = this.indexOf(item);
}
}
int listSize = this.list.size();
int[] deselect = ArrayTools.EMPTY_INT_ARRAY;
if (listSize > 0) {
deselect = ArrayTools.fill(new int[listSize], -1);
int i = 0;
for (int j = 0; j < listSize; j++) {
if ( ! ArrayTools.contains(select, j)) {
deselect[i++] = j;
}
}
}
int[] old = ArrayTools.sort(this.listBox.getSelectionIndices());
select = ArrayTools.sort(select);
if ( ! Arrays.equals(select, old)) {
this.listBox.deselect(deselect);
this.listBox.select(select);
}
}
public void dispose() {
this.listBox.removeSelectionListener(this.listBoxSelectionListener);
this.selectedItemsModel.removeCollectionChangeListener(CollectionValueModel.VALUES, this.selectedItemsListener);
this.selectedItems.clear();
}
// ********** selected items **********
/* CU private */ void selectedItemsAdded(CollectionAddEvent event) {
if ( ! this.listBox.isDisposed()) {
this.selectedItemsAdded_(event);
}
}
/**
* Modifying the list box's selected items programmatically does not
* trigger a {@link SelectionEvent}.
*/
private void selectedItemsAdded_(CollectionAddEvent event) {
@SuppressWarnings("unchecked")
Iterable<E> items = (Iterable<E>) event.getItems();
this.selectedItems.ensureCapacity(this.selectedItems.size() + event.getItemsSize());
CollectionTools.addAll(this.selectedItems, items);
int[] indices = new int[event.getItemsSize()];
int i = 0;
for (E item : items) {
indices[i++] = this.indexOf(item);
}
this.listBox.select(indices);
}
/* CU private */ void selectedItemsRemoved(CollectionRemoveEvent event) {
if ( ! this.listBox.isDisposed()) {
this.selectedItemsRemoved_(event);
}
}
/**
* Modifying the list box's selected items programmatically does not
* trigger a {@link SelectionEvent}.
*/
private void selectedItemsRemoved_(CollectionRemoveEvent event) {
@SuppressWarnings("unchecked")
Iterable<E> items = (Iterable<E>) event.getItems();
CollectionTools.removeAll(this.selectedItems, items);
int[] indices = new int[event.getItemsSize()];
int i = 0;
for (E item : items) {
indices[i++] = this.indexOf(item);
}
this.listBox.deselect(indices);
}
/* CU private */ void selectedItemsCleared() {
if ( ! this.listBox.isDisposed()) {
this.selectedItemsCleared_();
}
}
/**
* Modifying the list box's selected items programmatically does not
* trigger a {@link SelectionEvent}.
*/
private void selectedItemsCleared_() {
this.selectedItems.clear();
this.listBox.deselectAll();
}
/* CU private */ void selectedItemsChanged(CollectionChangeEvent event) {
if ( ! this.listBox.isDisposed()) {
this.selectedItemsChanged_(event);
}
}
private void selectedItemsChanged_(CollectionChangeEvent event) {
this.selectedItems.clear();
this.selectedItems.ensureCapacity(event.getCollectionSize());
@SuppressWarnings("unchecked")
Iterable<E> eventCollection = (Iterable<E>) event.getCollection();
CollectionTools.addAll(this.selectedItems, eventCollection);
this.setListSelection();
}
/**
* <strong>NB:</strong> an index of <code>-1</code> is ignored by
* {@link List} (lucky for us).
*/
private int indexOf(E item) {
int i = 0;
for (E each : this.list) {
if (ObjectTools.equals(each, item)) {
return i;
}
i++;
}
return -1;
}
// ********** list box events **********
/* CU private */ void listBoxSelectionChanged() {
this.selectedItemsModel.setValues(this.getListBoxSelectedItems());
}
/* CU private */ void listBoxDoubleClicked() {
this.listBoxSelectionChanged();
}
private Iterable<E> getListBoxSelectedItems() {
ArrayList<E> lbSelectedItems = new ArrayList<E>(this.listBox.getSelectionCount());
for (int selectionIndex : this.listBox.getSelectionIndices()) {
lbSelectedItems.add(this.list.get(selectionIndex));
}
return lbSelectedItems;
}
// ********** misc **********
@Override
public String toString() {
return ObjectTools.toString(this, this.selectedItems);
}
}