/* * Copyright (c) 2002,2009 Declarative Engineering LLC. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Declarative Engineering LLC * verson 1 which accompanies this distribution, and is available at * http://declarativeengineering.com/legal/DE_Developer_License_v1.txt */ package com.foundation.view.swt; import com.common.util.*; import com.common.util.optimized.*; /** * Used as a base class for collection components that are indexed. * Components such as a list of strings may be indexed such that objects from which the strings are derived cannot be attached to the row since there isn't the concept of a row. * This class tracks the mapping between the row value and the value's index in the collection to allow selections and updates to be converted to and from values and indices. */ public abstract class IndexedCollectionComponent extends CollectionComponent { /** A mapping of collection values by the control assigned indexes. */ private IIndexedCollection valueByIndexMap = new LiteList(100); /** A mapping of control assigned indices by the collection values. Since the item can be in the collection more than once we will either store an Integer or an IntArray. */ private LiteHashMap indexByValueMap = new LiteHashMap(100, com.common.comparison.Comparator.getLogicalComparator(), null); /** * IndexedCollectionComponent constructor. * @param parent The parent container. * @param name The name of the component. * @param style The style for the control. */ public IndexedCollectionComponent(Container parent, String name, int style) { super(parent, name, style); }//IndexedCollectionComponent()// /** * Gets the control specific data for a collection item. * @param item The item data which will eventually make it to the visual control. * @return The data for the item. This can be an array of values if necessary. */ protected abstract Object getItemData(Object item); /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#updateHiddenData(java.lang.Object, int, java.lang.Object) */ protected void updateHiddenData(Object item, int hiddenDataIndex, Object value) { if(getAllowMultiSelection()) { boolean updateSelectionLinks = false; int[] selectedIndices = controlGetSelections(); Object[] selectedValues = new Object[selectedIndices.length]; if((selectedIndices != null) && (selectedIndices.length > 0)) { //Determine whether any of the selections match with the hidden data's item to see whether we need to update any hidden data linkages.// for(int selectionIndex = 0; (!updateSelectionLinks) && (selectionIndex < selectedIndices.length); selectionIndex++) { Object selectionValue = valueByIndexMap.get(selectedIndices[selectionIndex]); updateSelectionLinks = selectionValue == item; selectedValues[selectionIndex] = selectionValue; }//for// }//if// if(updateSelectionLinks) { getHiddenData(hiddenDataIndex).invokeLinkage(selectedValues); }//if// }//if// else { int selectedIndex = controlGetSelection(); if(selectedIndex != -1) { Object selectedValue = valueByIndexMap.get(selectedIndex); if(selectedValue == item) { getHiddenData(hiddenDataIndex).invokeLinkage(selectedValue); }//if// }//if// }//else// }//updateHiddenData()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#updateSelectionLinks() */ protected void updateSelectionLinks() { super.updateSelectionLinks(); if(getHiddenDataCount() > 0) { if(getAllowMultiSelection()) { int[] selectedIndices = controlGetSelections(); Object[] selectedValues = selectedIndices != null && selectedIndices.length > 0 ? new Object[selectedIndices.length] : null; if(selectedValues != null) { //Collect the selected values.// for(int selectionIndex = 0; selectionIndex < selectedIndices.length; selectionIndex++) { selectedValues[selectionIndex] = valueByIndexMap.get(selectedIndices[selectionIndex]); }//for// }//if// //Update the hidden data linkages.// for(int hiddenDataIndex = 0; hiddenDataIndex < getHiddenDataCount(); hiddenDataIndex++) { getHiddenData(hiddenDataIndex).invokeLinkage(selectedValues); }//for// }//if// else { int selectedIndex = controlGetSelection(); Object selectedValue = selectedIndex != -1 ? valueByIndexMap.get(selectedIndex) : null; //Update the hidden data linkages.// for(int hiddenDataIndex = 0; hiddenDataIndex < getHiddenDataCount(); hiddenDataIndex++) { getHiddenData(hiddenDataIndex).invokeLinkage(selectedValue); }//for// }//else// }//if// }//updateSelectionLinks()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#internalViewRefreshCollection(com.common.util.ICollection, com.common.util.ICollection) */ protected void internalViewRefreshCollection(ICollection newCollection, ICollection oldCollection) { //TODO: Make this simpler - this is not at all as easy as the tcv code & is less useful.// if((newCollection != null) && (newCollection.getSize() > 0)) { Object[] items = new Object[newCollection.getSize()]; IIterator iterator = newCollection.iterator(); int removedCount = 0; //Remove all index mappings so we reflect the latest collection ordering (in case the user wants the order to reflect a new collection order).// valueByIndexMap.removeAll(); indexByValueMap.removeAll(); //Get the array of strings representing the list contents.// while(iterator.hasNext()) { Object value = iterator.next(); //Prevent two objects that are exactly the same from being added to the collection control.// if(indexByValueMap.containsKey(value)) { //Do nothing.// removedCount++; }//if// else { int valueIndex = addValueToMappings(value); //Add the value to the item array.// items[valueIndex] = getItemData(value); }//else// }//while// //Truncate the array so we don't have nulls at the end.// if(removedCount > 0) { Object[] newItems = new Object[items.length - removedCount]; System.arraycopy(items, 0, newItems, 0, newItems.length); items = newItems; }//if// //Set the collection contents.// controlSetItems(items); }//if// else { //Remove all existing items.// controlRemoveAll(); }//else// }//internalViewRefreshCollection()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#internalViewRefreshSelection(java.lang.Object) */ protected void internalViewRefreshSelection(Object selectedItem) { //TODO: Make this simpler - this is not at all as easy as the tcv code & is less useful.// int selectionIndex = controlGetSelection(); int modelSelectionIndex = -1; Object controlSelection = selectionIndex != -1 ? valueByIndexMap.get(selectionIndex) : null; Object indexData = selectedItem != null ? indexByValueMap.get(selectedItem) : null; if(indexData == null) { modelSelectionIndex = -1; }//if// else if(indexData instanceof Integer) { modelSelectionIndex = ((Integer) indexData).intValue(); }//else if// else { //Select the first one if the selection is not one of the possible ones.// if(!((IntArray) indexData).containsValue(modelSelectionIndex)) { modelSelectionIndex = ((IntArray) indexData).get(0); //Use the first occurance of the object.// }//if// }//else// if((getAllowUserItems()) && (selectedItem != null) && (indexData == null)) { //Set a custom selection for the control (currently only supported for string values).// controlSetCustomItem(selectedItem); selectionIndex = -1; }//if// else if(controlSelection != selectedItem) { //Set the control selection.// controlSetSelection(modelSelectionIndex); }//else if// }//internalViewRefreshSelection()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#internalViewRefreshSelections(com.common.util.ICollection, com.common.util.ICollection) */ protected void internalViewRefreshSelections(ICollection newSelections, ICollection oldSelections) { //TODO: Make this simpler - this is not at all as easy as the tcv code & is less useful.// if(newSelections != null) { int[] controlSelections = controlGetSelections(); boolean[] markedSelections = new boolean[controlSelections.length]; IIterator selectionIterator = newSelections.iterator(); for(int index = 0; index < markedSelections.length; index++) { markedSelections[index] = false; }//for// //Apply differences between the selection collection and the control selection. Also remove all impossible selections.// while(selectionIterator.hasNext()) { Object modelSelection = selectionIterator.next(); Object indexData = indexByValueMap.get(modelSelection); if(indexData == null) { //An invalid selection because the selection is not in the collection of displayed values.// selectionIterator.remove(); }//if// else if(indexData instanceof Integer) { int selectionIndex = ((Integer) indexData).intValue(); for(int index = 0; index < controlSelections.length; index++) { if(controlSelections[index] == selectionIndex) { if(!markedSelections[index]) { //The selection is already in the control.// markedSelections[index] = true; selectionIndex = -1; break; }//if// else { //Cannot have two of the same value selected if the value is only in the data collection once.// selectionIterator.remove(); selectionIndex = -1; }//else// }//if// }//for// //If we did not find the selected object in the array of currently selected indexes, then add it now.// if(selectionIndex != -1) { controlAddSelection(selectionIndex); }//if// }//else if// else { IntArray indices = (IntArray) indexData; boolean wasFound = false; //This is a complex bit of code that must as best as possible identify whether the selection is already represented in the control.// for(int index = 0; index < controlSelections.length; index++) { //If the control selection is one of the indexes assigned to the selected object then check to see if we have already identified it as used.// if(indices.containsValue(controlSelections[index])) { if(!markedSelections[index]) { //The selection is already in the control and has not already been marked.// markedSelections[index] = true; wasFound = true; break; }//if// }//if// }//for// //If we did not find the selected object in the array of currently selected indexes, then add it now.// if(!wasFound) { IIntIterator indicesIterator = indices.iterator(); boolean wasAdded = false; //Since we don't know exactly which one to add, we will have to search for one that is not selected.// //Note: We could seemingly do this faster by querying the control, but that would require a round trip query to the control which may be very expensive.// while((!wasAdded) && (indicesIterator.hasNext())) { int nextIndex = indicesIterator.next(); //Search the control selections for the next possible selection index.// for(int index = 0; index < controlSelections.length; index++) { if(nextIndex == controlSelections[index]) { nextIndex = -1; break; }//if// }//for// //If the next possible selection index was not in the control's selection collection then we can add it.// if(nextIndex != -1) { controlAddSelection(nextIndex); wasAdded = true; }//if// }//while// if(!wasAdded) { //Cannot have more references to a value in the selection collection than are in the data collection.// selectionIterator.remove(); }//if// }//if// }//else// }//while// //Remove all control selections that have not been marked. These selections were not represented in the model collection of selections.// for(int index = 0; index < markedSelections.length; index++) { if(!markedSelections[index]) { controlRemoveSelection(controlSelections[index]); }//if// }//for// }//if// else { //Remove all selections.// controlSetSelection(-1); }//else// }//internalViewRefreshSelections()// /** * Synchronizes the selection to the model from the view. *

This method has no logic to examine the auto synchronization state. It simply synchronizes the selection immediatly.

*/ protected void synchronizeSelection() { if(getAllowMultiSelection()) { int[] controlSelectionIndices = controlGetSelections(); ICollection modelSelections = getModelSelections(); //Prevent the selection additions from causing a feedback loop.// suspendSelectionHooks = true; try { if(modelSelections != null) { Object[] controlSelectionObjects = new Object[controlSelectionIndices.length]; boolean[] markedControlSelections = new boolean[controlSelectionObjects.length]; IIterator modelSelectionIterator = modelSelections.iterator(); LiteList removed = new LiteList(modelSelections.getSize()); LiteList added = new LiteList(controlSelectionIndices.length); for(int index = 0; index < controlSelectionIndices.length; index++) { controlSelectionObjects[index] = valueByIndexMap.get(controlSelectionIndices[index]); markedControlSelections[index] = false; }//for// //Add selected items to the selection collection.// while(modelSelectionIterator.hasNext()) { Object modelSelection = modelSelectionIterator.next(); boolean found = false; for(int index = 0; (!found) && (index < markedControlSelections.length); index++) { if(!markedControlSelections[index]) { if(controlSelectionObjects[index] == modelSelection) { markedControlSelections[index] = true; found = true; }//if// }//if// }//for// if(!found) { removed.add(modelSelection); }//if// }//while// for(int index = 0; index < markedControlSelections.length; index++) { if(!markedControlSelections[index]) { added.add(controlSelectionObjects[index]); }//if// }//for// modelSelections.replaceAll(removed, added); }//if// }//try// finally { suspendSelectionHooks = false; }//finally// }//if// else { int selectionIndex = controlGetSelection(); Object controlSelection = selectionIndex != -1 ? valueByIndexMap.get(selectionIndex) : null; if((getAllowUserItems()) && (selectionIndex == -1) && (controlGetText().length() > 0)) { setModelSelection(controlGetText()); }//if// else { setModelSelection(controlSelection); }//else if// }//else// }//internalViewSynchronizeSelection()// /** * Gets the index (or indices) for the given item. * @param item The item whose index is required. * @return The index data for the item which will either be an Integer, an IntArray, or null if the item is not mapped. */ protected Object getItemIndexData(Object item) { return (item != null) && (indexByValueMap.containsKey(item)) ? indexByValueMap.get(item) : null; }//getItemIndexData()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#itemAdded(java.lang.Object, int) */ protected void itemAdded(Object item, int index) { //Setup a mapping and a new index for the value.// index = addValueToMappings(item); //Add the value to the control.// controlAddItem(getItemData(item), index); //TODO: Should this be allowed? If the user sets the selection to an object not in the collection, should we prevent it? What about controls like a combobox which allow custom input?// if((!getAllowMultiSelection()) && (getModelSelection() == item)) { controlSetSelection(index); }//if// }//itemAdded()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#itemRemoved(java.lang.Object, int) */ protected void itemRemoved(Object item, int index) { if(indexByValueMap.containsKey(item)) { Object indexData = indexByValueMap.get(item); int modelIndex; //Determine whether the removed object occurs more than once in the collection.// if(indexData instanceof Integer) { modelIndex = ((Integer) indexData).intValue(); //Remove the item from the control and unregister all event handlers.// controlRemoveItem(modelIndex); }//if// else { IntArray indices = (IntArray) indexData; //TODO: We could make things look better by removing the selected object when there are more than one possible objects to be removed.// //Assume that we should remove the last occurance of the object.// modelIndex = indices.get(indices.getSize() - 1); //Remove the item from the control.// controlRemoveItem(modelIndex); }//else// //Remove the object from the mappings.// removeValueFromMappings(item, modelIndex); }//if// }//itemRemoved()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#itemAllRemoved() */ protected void itemAllRemoved() { //Remove all index mappings so we reflect the latest collection ordering (in case the user wants the order to reflect a new collection order).// valueByIndexMap.removeAll(); indexByValueMap.removeAll(); //Remove all existing items.// controlRemoveAll(); }//itemAllRemoved()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#registerItem(java.lang.Object) */ protected void registerItem(Object item) { internalItemAdded(item); }//registerItem()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#unregisterItem(java.lang.Object) */ protected void unregisterItem(Object item) { internalItemRemoved(item); }//unregisterItem()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#unregisterItems() */ protected void unregisterItems() { internalItemsRemoved(); }//unregisterItems()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#itemSorted(int[]) */ protected void itemSorted(int[] mapping) { //TODO: Re-order the indicies in the control and rebuild the mappings.// }//itemSorted()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#selectionAdded(java.lang.Object) */ protected void selectionAdded(Object value) { checkObjectSelectionCount(value); }//selectionAdded()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#selectionRemoved(java.lang.Object) */ protected void selectionRemoved(Object value) { checkObjectSelectionCount(value); }//selectionRemoved()// /* (non-Javadoc) * @see com.foundation.view.swt.CollectionComponent#selectionAllRemoved() */ protected void selectionAllRemoved() { controlRemoveAllSelections(); }//selectionAllRemoved()// /** * Verifies that the correct number of selections for the given value exist in the control. * @param value The value that is being checked. It may be checked because a selection was added, or removed. */ protected void checkObjectSelectionCount(Object value) { if(!getAllowMultiSelection()) { throw new RuntimeException("Cannot call this method: 'checkObjectSelectionCount' if multiple selections are not allowed."); }//if// if(indexByValueMap.containsKey(value)) { int[] controlSelectionIndices = controlGetSelections(); Object indexData = indexByValueMap.get(value); int possibleCount = indexData == null ? 0 : indexData instanceof Integer ? 1 : ((IntArray) indexData).getSize(); int modelCount = 0; IIterator iterator = getModelSelections().iterator(); //Count the number of times the object is in the model's selection collection. Remove impossible references.// while(iterator.hasNext()) { if(iterator.next() == value) { modelCount++; //Fix impossible selections.// if(modelCount > possibleCount) { modelCount = possibleCount; iterator.remove(); }//if// }//if// }//while// //Count the number of times the object is selected in the control's selections. Remove or add selections to match the model's selection collection.// if(indexData instanceof Integer) { int valueIndex = ((Integer) indexData).intValue(); int controlCount = 0; for(int index = 0; index < controlSelectionIndices.length; index++) { if(controlSelectionIndices[index] == valueIndex) { controlCount++; if(controlCount > modelCount) { controlRemoveSelection(valueIndex); }//if// //Stop iterating since there can only be one selection for this object.// break; }//if// }//for// if(controlCount < modelCount) { controlAddSelection(valueIndex); }//if// }//if// else { int controlCount = 0; IntArray valueIndices = new IntArray((IntArray) indexData); //Copy the object indices so we can remove the ones we already have used.// for(int index = 0; index < controlSelectionIndices.length; index++) { Object controlSelectionValue = valueByIndexMap.get(controlSelectionIndices[index]); if(controlSelectionValue == value) { controlCount++; if(controlCount > modelCount) { //Remove the selection because there are too many.// controlRemoveSelection(controlSelectionIndices[index]); }//if// else { //Remove used object indexes so it is easy to add more references if they are required.// valueIndices.remove(valueIndices.getIndexOf(controlSelectionIndices[index])); }//else// }//if// }//for// //Add selections for the object until the counts match.// while(controlCount < modelCount) { controlAddSelection(valueIndices.get(valueIndices.getSize())); valueIndices.remove(valueIndices.getSize()); controlCount++; }//while// }//else// }//if// }//checkObjectCount()// /** * Removes a value from the mappings of values and internal indexes. *

Note: The current implementation is very fast, except when removing items from a very large collection of values. This performance problem is due to the need to clean the object->index mapping after each removal.

* @param removedValue The value removed from the collection being displayed. * @param removedIndex The index of the removed value. This is necessary because a value could be in the collection more than one time. */ protected void removeValueFromMappings(Object removedValue, int removedIndex) { if(indexByValueMap.containsKey(removedValue)) { Object indexData = indexByValueMap.get(removedValue); IIterator iterator = indexByValueMap.keyIterator(); if(indexData instanceof IntArray) { IntArray indices = (IntArray) indexData; //Remove the specified index from the array of indices.// indices.remove(indices.getIndexOf(removedIndex)); //Optimize things by only using an IntArray when necessary.// if(indices.getSize() == 1) { indexByValueMap.put(removedValue, new Integer(indices.get(0))); }//if// }//if// else { //Since there should be no other occurances of the object in the collection we can remove the mapping altogether.// indexByValueMap.remove(removedValue); }//else// valueByIndexMap.remove(removedIndex); while(iterator.hasNext()) { Object next = iterator.next(); indexData = indexByValueMap.get(next); if(indexData instanceof Integer) { if(((Integer) indexData).intValue() > removedIndex) { indexByValueMap.put(next, new Integer(((Integer) indexData).intValue() - 1)); }//if// }//if// else { IntArray indices = (IntArray) indexData; //TODO: For large collections where items may be regularly removed we may wish to create a special collection that maintains the object to index mappings. We could use the bucket approach to maintaining the index number.// for(int index = indices.getSize() - 1; (index >= 0) && (indices.get(index) > removedIndex); index--) { indices.replace(index, indices.get(index) - 1); }//for// }//else// }//while// }//if// }//removeValueFromMappings()// /** * Adds a value to the mappings of values and internal indexes. * @param addedValue The value added to the collection being displayed. * @return The internal index of the added value. */ protected int addValueToMappings(Object addedValue) { int index = valueByIndexMap.getSize(); //Always add the value to the indexed mapping.// valueByIndexMap.add(addedValue); //Either add the index to the index by value map, or add it to a collection indexed by the object if the object is in the collection more than once.// if(!indexByValueMap.containsKey(addedValue)) { //Add the index to the mappings.// indexByValueMap.put(addedValue, new Integer(index)); }//if// else { Object indexData = indexByValueMap.get(addedValue); if(indexData instanceof Integer) { IntArray indices = new IntArray(10, 30); //Create a collection of indices for the same object.// indices.add(((Integer) indexData).intValue()); indices.add(index); indexByValueMap.put(addedValue, indices); }//if// else { //Add the new index to the collection of indices where this same object can be found in the control's list.// ((IntArray) indexData).add(index); }//else// }//else// return index; }//addValueToMappings()// /** * Removes all displayed items from the control. */ protected abstract void controlRemoveAll(); /** * Sets the control's displayed collection values ordered by index. * @param items The indexed values displayed by the control. */ protected abstract void controlSetItems(Object[] items); /** * Sets an item in the control's collection display. * @param index The index at which the item should be set. * @param itemData The value displayed by the control. */ protected abstract void controlSetItem(int index, Object itemData); /** * Adds an item to the control's collection display. * @param itemData The value displayed by the control. * @param index The index at which the item should be added. */ protected abstract void controlAddItem(Object itemData, int index); /** * Adds an item to the control's collection at the end. * @param itemData The value displayed by the control. */ protected abstract void controlAddItem(Object itemData); /** * Removes an item from the control's collection display. * @param index The index of the item to be removed. */ protected abstract void controlRemoveItem(int index); /** * Gets the indices selected within the control. * @return The collection of indices selected. */ protected abstract int[] controlGetSelections(); /** * Gets the index selected within the control. * @return The index currently selected. */ protected abstract int controlGetSelection(); /** * Sets the selected values in the control. * @param indices The indices of the selected values. */ protected abstract void controlSetSelections(int[] indices); /** * Sets the selected value in the control. * @param index The index of the selected value. */ protected abstract void controlSetSelection(int index); /** * Adds a selected value in the control. * @param index The index of the selected value. */ protected abstract void controlAddSelection(int index); /** * Deselects a value in the control. * @param index The index of the selected value. */ protected abstract void controlRemoveSelection(int index); /** * Deselects all values in the control. */ protected abstract void controlRemoveAllSelections(); /** * Checks to see if a value is selected. * @param index The index of the checked value. * @return Whether the value is currently selected. */ protected abstract boolean controlIsSelected(int index); /** * Gets the current text for the control. *

Some controls such as comboboxes have a text which may not be one of the listed items.

* @return The control's current text. */ protected abstract String controlGetText(); /** * Sets the current custom selection item for the control. *

Some controls such as comboboxes have a text which may not be one of the listed items.

* @param item The controls custom selected item. */ protected abstract void controlSetCustomItem(Object item); /** * Forces the selection event handler to be called. */ protected abstract void forceSelectionEvent(); //TODO: Should call widgetSelected implemented by the subclass. This name may need to change.// }//IndexedCollectionComponent//