/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Michael Powers This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.web; import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Observable; import net.wotonomy.control.EODataSource; import net.wotonomy.control.EOEditingContext; import net.wotonomy.control.EOKeyValueCoding; import net.wotonomy.control.EOKeyValueCodingSupport; import net.wotonomy.control.EOObjectStore; import net.wotonomy.control.EOObserverCenter; import net.wotonomy.control.EOObserving; import net.wotonomy.control.EOQualifier; import net.wotonomy.control.EOSortOrdering; import net.wotonomy.control.OrderedDataSource; import net.wotonomy.control.PropertyDataSource; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSMutableArray; import net.wotonomy.foundation.NSMutableDictionary; import net.wotonomy.foundation.NSNotification; import net.wotonomy.foundation.NSNotificationCenter; import net.wotonomy.foundation.NSRange; import net.wotonomy.foundation.NSSelector; import net.wotonomy.foundation.internal.Duplicator; import net.wotonomy.foundation.internal.WotonomyException; /** * WODisplayGroup manages a set of objects, allowing them to be sorted, batched, * and filtered. WODisplay also acts as a bridge to the wotonomy's control * package, including WODisplayGroup and EOEditingContext. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 905 $ */ public class WODisplayGroup extends Observable implements EOObserving, EOEditingContext.Editor, java.io.Serializable { /** * Notification sent when the display group is about to fetch. */ public static final String DisplayGroupWillFetchNotification = "DisplayGroupWillFetchNotification"; private static boolean globalDefaultForValidatesChangesImmediately = true; private static String globalDefaultStringMatchFormat = "caseInsensitiveLike"; private static String globalDefaultStringMatchOperator = "%@*"; protected NSMutableArray allObjects; protected NSArray allObjectsProxy; protected NSMutableArray displayedObjects; protected NSArray displayedObjectsProxy; protected NSMutableArray selectedObjects; protected NSArray selectedObjectsProxy; protected NSMutableArray selectedIndexes; private String defaultStringMatchOperator; private String defaultStringMatchFormat; private boolean validatesChangesImmediately; private Object delegate; private EODataSource dataSource; private EOQualifier qualifier; private NSMutableArray sortOrderings; private NSArray sortOrderingsProxy; private NSArray localKeys; private NSDictionary insertedObjectDefaultValues; private boolean fetchesOnLoad; private boolean selectsFirstObjectAfterFetch; private boolean usesOptimisticRefresh; private boolean inQueryMode; // change detection: package access for helper classes boolean selectionChanged; int updatedObjectIndex; // batching private int batchIndex; private int batchSize; // this property is not in the spec private boolean compareByReference = false; private EOObserving lastGroupObserver; /** * Creates a new display group. */ public WODisplayGroup() { validatesChangesImmediately = globalDefaultForValidatesChangesImmediately(); defaultStringMatchOperator = globalDefaultStringMatchFormat(); defaultStringMatchFormat = globalDefaultStringMatchOperator(); allObjects = new ObservableArray(this); allObjectsProxy = NSArray.arrayBackedByList(allObjects); displayedObjects = new NSMutableArray(); displayedObjectsProxy = NSArray.arrayBackedByList(displayedObjects); selectedObjects = new NSMutableArray(); selectedObjectsProxy = NSArray.arrayBackedByList(selectedObjects); sortOrderings = new NSMutableArray(); sortOrderingsProxy = NSArray.arrayBackedByList(sortOrderings); selectedIndexes = new NSMutableArray(); delegate = null; dataSource = null; qualifier = null; localKeys = new NSArray(); // not implemented insertedObjectDefaultValues = new NSDictionary(); fetchesOnLoad = false; // not implemented selectsFirstObjectAfterFetch = false; usesOptimisticRefresh = false; inQueryMode = false; // not implemented selectionChanged = false; updatedObjectIndex = -1; batchIndex = 0; batchSize = 0; } // specify optional data source /** * Sets the data source that will be used by this display group. */ public void setDataSource(EODataSource aDataSource) { if ((dataSource != null) && (dataSource.editingContext() != null)) { // un-register for notifications from existing parent store NSNotificationCenter.defaultCenter().removeObserver(this, null, dataSource.editingContext()); dataSource.editingContext().removeEditor(this); if (dataSource.editingContext().messageHandler() == this) { dataSource.editingContext().setMessageHandler(null); } } dataSource = aDataSource; if ((dataSource != null) && (dataSource.editingContext() != null)) { // register for notifications from parent store NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("objectsInvalidatedInEditingContext", new Class[] { NSNotification.class }), null, dataSource.editingContext()); // add ourselves as editor dataSource.editingContext().addEditor(this); // add ourselves as message handler if no such handler exists if (dataSource.editingContext().messageHandler() == null) { dataSource.editingContext().setMessageHandler(this); } } } /** * Returns the current data source backing this display group, or null if no * dataSource is currently used. */ public EODataSource dataSource() { return dataSource; } /** * Returns the key by which this display group is bound a master display group, * or null if this is not a detail display group. */ public String detailKey() { if (dataSource instanceof PropertyDataSource) { return ((PropertyDataSource) dataSource).key(); } return null; } /** * Sets the key by which this display group is bound a master display group. * Does nothing if this is not a detail display group. */ public void setDetailKey(String aKey) { if (dataSource instanceof PropertyDataSource) { ((PropertyDataSource) dataSource).setKey(aKey); } } /** * Returns whether the data source is a detail data source, suggesting that this * is a detail display group. */ public boolean hasDetailDataSource() { return (dataSource instanceof PropertyDataSource); } /** * Returns the selected object in the master display group, or null if this is * not a detail display group. */ public Object masterObject() { if (dataSource instanceof PropertyDataSource) { return ((PropertyDataSource) dataSource).source(); } return null; } /** * Sets the master object in the detail data source. Does nothing if there is no * detail data source. */ public void setMasterObject(Object anObject) { if (dataSource instanceof PropertyDataSource) { ((PropertyDataSource) dataSource).setSource(anObject); } } // specify optional delegate /** * Sets the display group delegate that will be used by this display group. */ public void setDelegate(Object aDelegate) { delegate = aDelegate; } /** * Returns the current delegate for this display group, or null if no delegate * is currently set. */ public Object delegate() { return delegate; } // display group configuration /** * Returns the current string matching format. If not set, defaults to "%@*". */ public String defaultStringMatchFormat() { return defaultStringMatchFormat; } /** * Returns the current string matching operator. If not set, defaults to * "caseInsensitiveLike". */ public String defaultStringMatchOperator() { return defaultStringMatchOperator; } /** * Sets the display group and associations to edit a "query by example" query * object. This method is used for target/action connections. */ public void enterQueryMode(Object aSender) { throw new RuntimeException("Not implemented yet."); } /** * Returns whether this display group should immediate fetch when loaded. */ public boolean fetchesOnLoad() { return fetchesOnLoad; } /** * Returns whether this display group is in "query by example" mode. */ public boolean inQueryMode() { return inQueryMode; } /** * Returns a Map of default values that are applied to new objects that are * inserted into the list. */ public NSDictionary insertedObjectDefaultValues() { return insertedObjectDefaultValues; } /** * Returns the keys that were declared when read from an external resource file. */ public NSArray localKeys() { return localKeys; } /** * Sets whether this display group will select the first object in the list * after a fetch. */ public boolean selectsFirstObjectAfterFetch() { return selectsFirstObjectAfterFetch; } /** * Sets the default string matching format that will be used by this display * group. */ public void setDefaultStringMatchFormat(String aFormat) { throw new RuntimeException("Not implemented yet."); } /** * Sets the default string matching operator that will be used by this display * group. */ public void setDefaultStringMatchOperator(String anOperator) { throw new RuntimeException("Not implemented yet."); } /** * Sets whether this display group will fetch objects from its data source on * load. */ public void setFetchesOnLoad(boolean willFetch) { fetchesOnLoad = willFetch; } /** * Sets whether this display group is in "query by example" mode. If true, all * associations will bind to a special "example" object. */ public void setInQueryMode(boolean isInQueryMode) { inQueryMode = isInQueryMode; } /** * Sets the mapping that contains the values that will be applied to new objects * inserted into the display group. */ public void setInsertedObjectDefaultValues(Map aMap) { insertedObjectDefaultValues = new NSDictionary(aMap); } /** * Sets the keys that are declared when instantiated from an external resource * file. */ public void setLocalKeys(List aKeyList) { localKeys = new NSArray((Collection) aKeyList); } /** * Sets whether the first object in the list will be selected after a fetch. */ public void setSelectsFirstObjectAfterFetch(boolean selectsFirst) { selectsFirstObjectAfterFetch = selectsFirst; } /** * Sets the order of the keys by which this display group will be ordered after * a fetch or after a call to updateDisplayedObjects(). The elements in the * display group will be sorted first by the first key, within the first key, by * the second key, and so on. */ public void setSortOrderings(List aList) { sortOrderings.removeAllObjects(); Object o; Iterator it = aList.iterator(); while (it.hasNext()) { o = it.next(); // handle the convenience of specifying just a key if (!(o instanceof EOSortOrdering)) { o = new EOSortOrdering(o.toString(), EOSortOrdering.CompareAscending); } sortOrderings.add(o); } } /** * Sets whether only changed objects are refreshed (optimistic), or whether all * objects are refreshed (pessimistic, default). By default, when the display * group receives notification that one of its objects has changed, * updateDisplayedObjects is called. */ public void setUsesOptimisticRefresh(boolean isOptimistic) { usesOptimisticRefresh = isOptimistic; } /** * Sets whether changes made by associations are validated immediately, or when * changes are saved. */ public void setValidatesChangesImmediately(boolean validatesImmediately) { validatesChangesImmediately = validatesImmediately; } /** * Returns a read-only List of sort orderings for this display group. */ public NSArray sortOrderings() { return sortOrderingsProxy; } /** * Returns whether this display group refreshes only the changed objects or all * objects on refresh. */ public boolean usesOptimisticRefresh() { return usesOptimisticRefresh; } /** * Returns whether this display group validates changes immediately. Otherwise, * validation should occur when changes are saved. Default is the global * default, which is initially true. */ public boolean validatesChangesImmediately() { return validatesChangesImmediately; } // qualification /** * Returns a qualifier that will be applied all the objects in this display * group to determine which objects will be displayed. */ public EOQualifier qualifier() { return qualifier; } /** * Returns a new qualifier built from the three query value maps: greater than, * equal to, and less than. */ public EOQualifier qualifierFromQueryValues() { // TODO: assemble qualifier from query values return new EOQualifier() { // use inner class until we actually implement one public EOQualifier qualifierWithBindings(Map aMap, boolean requireAll) { return null; } public Throwable validateKeysWithRootClassDescription(Class aClass) { return null; } public boolean evaluateWithObject(Object o) { return false; } }; } /** * Calls qualifierFromQueryValues(), applies the result to the data source, and * calls fetch(). */ public void qualifyDataSource() { throw new RuntimeException("Not implemented yet."); } /** * Calls qualifierFromQueryValues(), sets the qualifier with setQualifier(), and * calls updateDisplayedObjects(). */ public void qualifyDisplayGroup() { setQualifier(qualifierFromQueryValues()); updateDisplayedObjects(); } /** * Returns a Map containing the mappings of keys to binding query values. */ public NSDictionary queryBindingValues() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to binding query values. */ public NSMutableDictionary queryBindings() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to binding query values for a * matching query. */ public NSMutableDictionary queryMatch() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to binding query values for a * minimum value query. */ public NSMutableDictionary queryMin() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to binding query values for a * maximum value query. */ public NSMutableDictionary queryMax() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to operator values. */ public NSMutableDictionary queryOperator() { throw new RuntimeException("Not implemented yet."); } /** * Returns a list containing all supported qualifier operators. */ public NSArray allQualifierOperators() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to operator values. */ public NSDictionary queryOperatorValues() { throw new RuntimeException("Not implemented yet."); } /** * Sets the qualifier that will be used by updateDisplayedObjects() to filter * displayed objects. */ public void setQualifier(EOQualifier aQualifier) { qualifier = aQualifier; } /** * Sets the mapping that contains the mappings of keys to binding values. */ public void setQueryBindingValues(Map aMap) { throw new RuntimeException("Not implemented yet."); } /** * Sets the mapping that contains the mappings of keys to operator values. */ public void setQueryOperatorValues(Map aMap) { throw new RuntimeException("Not implemented yet."); } // qualifier query values /** * Returns a Map containing the mappings of keys to query values that will be * used to test for equality. */ public NSDictionary equalToQueryValues() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to query values that will be * used to test for greater value. */ public NSDictionary greaterThanQueryValues() { throw new RuntimeException("Not implemented yet."); } /** * Returns a Map containing the mappings of keys to query values that will be * used to test for lesser value. */ public NSDictionary lessThanQueryValues() { throw new RuntimeException("Not implemented yet."); } /** * Sets the Map that contains the mappings of keys to query values that will be * used to test for equality. */ public void setEqualToQueryValues(Map aMap) { throw new RuntimeException("Not implemented yet."); } /** * Sets the mapping that contains the mappings of keys to query values that will * be used to test for greater value. */ public void setGreaterThanQueryValues(Map aMap) { throw new RuntimeException("Not implemented yet."); } /** * Sets the mapping that contains the mappings of keys to query values that will * be used to test for lesser value. */ public void setLessThanQueryValues(Map aMap) { throw new RuntimeException("Not implemented yet."); } /** * Deprecated: returns true. */ public boolean endEditing() { return true; } // object management /** * Returns a read-only List containing all objects managed by the display group. * This includes those objects not visible due to disqualification. */ public NSArray allObjects() { // System.out.println( "avoided allocation: allObjects" ); return allObjectsProxy; } /** * Returns the total number of batches held by this display group. */ public int batchCount() { if (batchSize < 1) return 1; int count = displayedObjects.count(); if (count == 0) return 1; return ((count - 1) / batchSize) + 1; } /** * Returns the index of the currently displayed batch. */ public int currentBatchIndex() { return batchIndex; } /** * Sets the index of the currently displayed batch. */ public void setCurrentBatchIndex(int aBatchIndex) { batchIndex = aBatchIndex; updateDisplayedObjects(); } /** * Sets the displayed objects to the batch containing the first selected object, * and updates the current batch index, and returns null to force a page reload. * Displays the first batch is there is no selection. */ public Object displayBatchContainingSelectedObject() { if (batchSize < 1) return null; NSArray indexes = selectionIndexes(); if (indexes.count() > 0) { batchIndex = ((Number) indexes.objectAtIndex(0)).intValue() / batchSize; } else { batchIndex = 0; } updateDisplayedObjects(); return null; } /** * Sets the displayed objects to the next batch and updates the current batch * index, and returns null to force a page reload. If there is no next batch the * first batch is displayed. */ public Object displayNextBatch() { batchIndex = (batchIndex + 1) % batchCount(); updateDisplayedObjects(); return null; } /** * Sets the displayed objects to the next batch and updates the current batch * index, and returns null to force a page reload. If there is no previous * batch, the last batch is displayed. */ public Object displayPreviousBatch() { batchIndex--; if (batchIndex < 0) batchIndex = batchCount() - 1; updateDisplayedObjects(); return null; } /** * Returns whether the displayed objects have been batched. */ public boolean hasMultipleBatches() { return batchCount() > 1; } /** * Returns the one-based index within the displayed objects list of the first * displayed object in the current batch. */ public int indexOfFirstDisplayedObject() { if (batchSize < 1) return 1; return batchIndex * batchSize + 1; } /** * Returns the one-based index within the displayed objects list of the first * last object in the current batch. */ public int indexOfLastDisplayedObject() { if (batchSize < 1) return displayedObjects.count(); return Math.min(((batchIndex + 1) * batchSize), displayedObjects.count()); } /** * Returns the number of objects per batch. */ public int numberOfObjectsPerBatch() { return batchSize; } /** * Returns the number of objects per batch. */ public void setNumberOfObjectsPerBatch(int aSize) { batchSize = aSize; updateDisplayedObjects(); } /** * Clears the current selection. * * @return True is the selection was cleared, False if the selection could not * be cleared * @see #setSelectionIndexes */ public boolean clearSelection() { Object result = notifyDelegate("displayGroupShouldChangeSelection", new Class[] { WODisplayGroup.class, List.class }, new Object[] { this, new NSArray(selectedObjects) }); if ((result != null) && (Boolean.FALSE.equals(result))) { return false; } selectionChanged = true; willChange(); selectedObjects.removeAllObjects(); selectedIndexes.removeAllObjects(); notifyDelegate("displayGroupDidChangeSelection", new Class[] { WODisplayGroup.class }, new Object[] { this }); notifyDelegate("displayGroupDidChangeSelectedObjects", new Class[] { WODisplayGroup.class }, new Object[] { this }); return true; } /** * Convenience for binding to a component action: calls deleteSelection() and * then displayBatchContainingSelectedObject() and returns null, which is a * suitable result from a component action. */ public Object delete() { deleteSelection(); displayBatchContainingSelectedObject(); return null; } /** * Deletes the object at the specified index, notifying the delegate before and * after the operation, and then updating the selection if needed. * * @return True if delete was successful, false if the object was not deleted. */ public boolean deleteObjectAtIndex(int anIndex) { Object target = displayedObjects.objectAtIndex(anIndex); Object result = notifyDelegate("displayGroupShouldDeleteObject", new Class[] { WODisplayGroup.class, Object.class }, new Object[] { this, target }); if ((result != null) && (Boolean.FALSE.equals(result))) { return false; } deleteObjectAtIndexNoNotify(anIndex); if (dataSource != null) { dataSource.deleteObject(target); } notifyDelegate("displayGroupDidDeleteObject", new Class[] { WODisplayGroup.class, Object.class }, new Object[] { this, target }); return true; } private void deleteObjectAtIndexNoNotify(int anIndex) { Object target = displayedObjects.objectAtIndex(anIndex); int i; // remove from selected objects if necessary i = indexOf(selectedObjects, target); if (i != NSArray.NotFound) { selectionChanged = true; willChange(); // notify before removing selectedObjects.removeObjectAtIndex(i); selectedIndexes.remove(new Integer(i)); // comps by value } else // notify - no selection change needed { willChange(); } // remove from all objects i = indexOf(allObjects, target); if (i != NSArray.NotFound) { allObjects.removeObjectAtIndex(i); } else // otherwise should never happen { // throw new WotonomyException( // "Displayed object not found in allObjects" ); } // remove from displayed objects displayedObjects.removeObjectAtIndex(anIndex); } /** * Deletes the currently selected objects. This implementation calls * deleteObjectAtIndex() for each index in the selection list, immediately * returning false if any delete operation fails. * * @return True if all selected objects were deleted, false if any deletion * failed. */ public boolean deleteSelection() { int i; boolean result = true; Enumeration e = new NSArray(selectedObjects).objectEnumerator(); while (e.hasMoreElements()) { i = indexOf(displayedObjects, e.nextElement()); if (i == NSArray.NotFound) { // should never happen throw new WotonomyException("Selected object not found in displayedObjects"); } result = result && deleteObjectAtIndex(i); } return result; } /** * Returns a read-only List of all objects in the display group that are * currently displayed by the associations. */ public NSArray displayedObjects() { // System.out.println( "avoided allocation: displayedObjects" ); if (batchSize < 1) return displayedObjectsProxy; return displayedObjectsProxy.subarrayWithRange(new NSRange(batchIndex * batchSize, batchSize)); } /** * Requests a list of objects from the DataSource and calls setObjectArray to * populate the list. More specifically, calls endEditing(), asks the delegate, * fetches the objects, notifies the delegate, and populates the list. Returns * null to force a page reload. */ public Object fetch() { endEditing(); if (dataSource == null) { return null; } Object result = notifyDelegate("displayGroupShouldFetch", new Class[] { WODisplayGroup.class }, new Object[] { this }); if ((result != null) && (Boolean.FALSE.equals(result))) { return null; } NSNotificationCenter.defaultCenter().postNotification(DisplayGroupWillFetchNotification, this, new NSDictionary()); NSArray objectList = dataSource.fetchObjects(); notifyDelegate("displayGroupDidFetchObjects", new Class[] { WODisplayGroup.class, List.class }, new Object[] { this, objectList }); setObjectArray(objectList); if ((selectsFirstObjectAfterFetch) && (displayedObjects.size() > 0)) { setSelectionIndexes(new NSArray(new Integer(0))); } return null; } /** * Convenience to call insertNewObjectAtIndex with the current selection plus * one, or at the end of the list if there is no selection. Returns null to * force a page reload. */ public Object insert() { NSArray indexes = selectionIndexes(); int size = indexes.count(); if (size == 0) { insertNewObjectAtIndex(displayedObjects.count()); } else { insertNewObjectAtIndex(((Number) selectedIndexes.objectAtIndex(size - 1)).intValue() + 1); } return null; } /** * Creates a new object at the specified index. Calls insertObjectAtIndex() with * the result from sending createObject() to the data source. * * @return the newly created object. */ public Object insertNewObjectAtIndex(int anIndex) { Object result = null; if (dataSource != null) { result = dataSource.createObject(); } if (result != null) { if (insertedObjectDefaultValues != null) { Duplicator.writePropertiesForObject(insertedObjectDefaultValues, result); } insertObjectAtIndex(result, anIndex); } else // create failed { if (delegate() != null) { NSSelector selector = new NSSelector("displayGroupCreateObjectFailed", new Class[] { WODisplayGroup.class, EODataSource.class }); if (selector.implementedByObject(delegate())) { try { selector.invoke(delegate(), new Object[] { this, dataSource }); return result; } catch (Exception exc) { System.err.println("Error notifying delegate: displayGroupCreateObjectFailed"); exc.printStackTrace(); } } } } return result; } /** * Inserts the specified object into the list at the specified index. */ public void insertObjectAtIndex(Object anObject, int anIndex) { Object result = notifyDelegate("displayGroupShouldInsertObject", new Class[] { WODisplayGroup.class, Object.class, int.class }, new Object[] { this, anObject, new Integer(anIndex) }); if ((result != null) && (Boolean.FALSE.equals(result))) { return; } updatedObjectIndex = anIndex; willChange(); int i; // add to all objects if (anIndex == displayedObjects.size()) { allObjects.addObject(anObject); } else // insert before same object { Object target = displayedObjects.objectAtIndex(anIndex); int targetIndex = indexOf(allObjects, target); if (targetIndex != NSArray.NotFound) { allObjects.insertObjectAtIndex(anObject, targetIndex); } else // should never happen { throw new WotonomyException("Could not find displayed object in all objects list: " + target); } } // add to displayed objects displayedObjects.insertObjectAtIndex(anObject, anIndex); if (dataSource != null) { if (dataSource instanceof OrderedDataSource) { ((OrderedDataSource) dataSource).insertObjectAtIndex(anObject, anIndex); } else { dataSource.insertObject(anObject); } } notifyDelegate("displayGroupDidInsertObject", new Class[] { WODisplayGroup.class, Object.class }, new Object[] { this, anObject }); } /** * Sets contentsChanged to true and notifies all observers. */ public void redisplay() { willChange(); } /** * Sets the selection to the next displayed object after the current selection. * If the last object is selected, or if no object is selected, then the first * object becomes selected. If multiple items are selected, the first selected * item is considered the selected item for the purposes of this method. Does * not call redisplay(). * * @return null to force a page reload. */ public Object selectNext() { int count = displayedObjects.count(); if (count == 0) return null; if (count == 1) { selectObject(displayedObjects.objectAtIndex(0)); return null; } int i = -1; Object selectedObject = selectedObject(); if (selectedObject != null) { i = indexOf(displayedObjects, selectedObject); } if (i == NSArray.NotFound) i = -1; // select next object i++; if (i != displayedObjects.count()) { // set to next object selectedObject = displayedObjects.objectAtIndex(i); } else // out of range { // set to null selectedObject = displayedObjects.objectAtIndex(0); } selectObject(selectedObject); return null; } /** * Sets the selection to the specified object. If the specified object is null * or does not exist in the list of displayed objects, the selection will be * cleared. * * @return true if the object was selected. */ public boolean selectObject(Object anObject) { if ((anObject == null) || (indexOf(displayedObjects, anObject) == NSArray.NotFound)) { clearSelection(); return false; } selectObjectsIdenticalTo(new NSArray(new Object[] { anObject })); return true; } /** * Sets the selection to the specified objects. If the specified list is null or * if none of the objects in the list exist in the list of displayed objects, * the selection will be cleared. * * @return true if all specified objects were selected. */ public boolean selectObjectsIdenticalTo(List anObjectList) { // optimization: check for resetting of selection if ((anObjectList != null) && (selectedObjects.size() == anObjectList.size())) { boolean identical = true; int size = selectedObjects.size(); for (int i = 0; (i < size) && identical; i++) { // compare by reference if (anObjectList.get(i) != selectedObjects.get(i)) { identical = false; } else if (displayedObjects.indexOfIdenticalObject(anObjectList.get(i)) == NSArray.NotFound) { identical = false; } } if (identical) { return true; } } Object result = notifyDelegate("displayGroupShouldChangeSelection", new Class[] { WODisplayGroup.class, List.class }, new Object[] { this, anObjectList }); if ((result != null) && (Boolean.FALSE.equals(result))) { // need to notify the calling component // to revert back to the previous selection selectionChanged = true; willChange(); return false; } int i; selectionChanged = true; willChange(); Object o; selectedObjects.removeAllObjects(); selectedIndexes.removeAllObjects(); Iterator it = anObjectList.iterator(); while (it.hasNext()) { o = it.next(); if ((i = displayedObjects.indexOfIdenticalObject(o)) != NSArray.NotFound) { selectedObjects.addObject(o); selectedIndexes.addObject(new Integer(i)); } } notifyDelegate("displayGroupDidChangeSelection", new Class[] { WODisplayGroup.class }, new Object[] { this }); notifyDelegate("displayGroupDidChangeSelectedObjects", new Class[] { WODisplayGroup.class }, new Object[] { this }); return true; } /** * Calls selectObjectsIdenticalTo and if false is returned and * selectFirstIfNoMatch is true, selects the first object. */ public boolean selectObjectsIdenticalToSelectFirstOnNoMatch(List anObjectList, boolean selectFirstIfNoMatch) { if (selectObjectsIdenticalTo(anObjectList)) { return true; } if (selectFirstIfNoMatch) { clearSelection(); selectNext(); return true; } return false; } /** * Sets the selection to the previous displayed object before the current * selection. If the first object is selected, or if no object is selected, then * the last object becomes selected. If multiple items are selected, the first * selected item is considered the selected item for the purposes of this * method. Does not call redisplay(). * * @return null to force a page reload. */ public Object selectPrevious() { int i = displayedObjects.count(); if (i == 0) return null; if (i == 1) { selectObject(displayedObjects.objectAtIndex(0)); return null; } Object selectedObject = selectedObject(); if (selectedObject != null) { i = indexOf(displayedObjects, selectedObject); } if (i == NSArray.NotFound) i = displayedObjects.count(); // select next object i--; if (i < 0) { // out of range - select last object i = displayedObjects.count() - 1; } selectObject(displayedObjects.objectAtIndex(i)); return null; } /** * Returns the currently selected object, or null if there is no selection. */ public Object selectedObject() { if (selectedObjects.count() == 0) { return null; } return selectedObjects.objectAtIndex(0); } /** * Returns a read-only List containing all selected objects, if any. Returns an * empty list if no objects are selected. */ public NSArray selectedObjects() { // System.out.println( "avoided allocation: selectedObjects" ); return selectedObjectsProxy; } /** * Returns a read-only List containing the indexes of all selected objects, if * any. The list contains instances of java.lang.Number; call intValue() to * retrieve the index. */ public NSArray selectionIndexes() { // return selectedIndexes; int i; NSMutableArray result = new NSMutableArray(); Enumeration e = selectedObjects.objectEnumerator(); while (e.hasMoreElements()) { i = indexOf(displayedObjects, e.nextElement()); if (i != NSArray.NotFound) { result.addObject(new Integer(i)); } else { System.err.println("Should never happen: selected objects not in displayed objects"); new RuntimeException().printStackTrace(System.err); } } return result; } /** * Sets the objects managed by this display group. updateDisplayedObjects() is * called to filter the display objects. The previous selection will be * maintained if possible. The data source is not notified. */ public void setObjectArray(List anObjectList) { if (anObjectList == null) anObjectList = new NSArray(); Object result = notifyDelegate("displayGroupDisplayArrayForObjects", new Class[] { WODisplayGroup.class, List.class }, new Object[] { this, anObjectList }); if (result != null) { anObjectList = (List) result; } willChange(); NSArray oldSelectedObjects = new NSArray(selectedObjects); // copy // reset allObjects to new list allObjects.removeAllObjects(); allObjects.addObjectsFromArray(anObjectList); // update the displayed object list updateDisplayedObjects(); // restore the selection if possible selectObjectsIdenticalTo(oldSelectedObjects); batchIndex = 0; displayBatchContainingSelectedObject(); } /** * Sets the currently selected object, or clears the selection if the object is * not found or is null. Note: it's not clear how this differs from selectObject * in the spec. It is recommended that you call selectObject for now. */ public void setSelectedObject(Object anObject) { selectObject(anObject); } /** * Sets the current selection to the specified objects. The previous selection * is cleared, and any objects in the display group that are in the specified * list are then selected. If no items in the specified list are found in the * display group, then the selection is effectively cleared. Note: it's not * clear how this differs from selectObjectsIdenticalTo in the spec. It is * recommended that you call that method for now. */ public void setSelectedObjects(List aList) { selectObjectsIdenticalTo(aList); } /** * Sets the current selection to the objects at the specified indexes. Items in * the list are assumed to be instances of java.lang.Number. The previous * selection is cleared, and any objects in the display group that are in the * specified list are then selected. If no items in the specified list are found * in the display group, then the selection is effectively cleared. */ public boolean setSelectionIndexes(List aList) { Object o; int index; NSMutableArray objects = new NSMutableArray(); Iterator it = aList.iterator(); while (it.hasNext()) { index = ((Number) it.next()).intValue(); if (index < displayedObjects.count()) { o = displayedObjects.objectAtIndex(index); if (o != null) { objects.add(o); } } } return selectObjectsIdenticalTo(objects); } /** * Applies the qualifier to all objects and sorts the results to update the list * of displayed objects. Observing associations are notified to reflect the * changes. */ public void updateDisplayedObjects() { updatedObjectIndex = -1; willChange(); displayedObjects.removeAllObjects(); displayedObjects.addObjectsFromArray(allObjects); // apply qualifier, if any if (qualifier != null) { EOQualifier.filterArrayWithQualifier(displayedObjects, qualifier); } // apply sort orderings, if any NSArray orderings = sortOrderings(); if (orderings != null) { if (orderings.count() > 0) { selectionChanged = true; willChange(); EOSortOrdering.sortArrayUsingKeyOrderArray(displayedObjects, orderings); } } // make sure the selectedObjects is a subset of displayedObjects int i; Object o; Iterator it = new LinkedList(selectedObjects).iterator(); boolean removeflag = false; selectedIndexes.removeAllObjects(); while (it.hasNext()) { o = it.next(); if ((i = displayedObjects.indexOfIdenticalObject(o)) == NSArray.NotFound) { selectedObjects.removeIdenticalObject(o); removeflag = true; } else { selectedIndexes.addObject(new Integer(i)); } } // Note: it is important to put the // selectionChanged = true line below remove. if (removeflag) { selectionChanged = true; willChange(); notifyDelegate("displayGroupDidChangeSelection", new Class[] { WODisplayGroup.class }, new Object[] { this }); notifyDelegate("displayGroupDidChangeSelectedObjects", new Class[] { WODisplayGroup.class }, new Object[] { this }); } } /** * Returns the index of the changed object. If more than one object has changed, * -1 is returned. */ public int updatedObjectIndex() { return updatedObjectIndex; } // getting and setting values in objects /** * Returns a value on the selected object for the specified key. */ public Object selectedObjectValueForKey(String aKey) { Object selectedObject = selectedObject(); if (selectedObject == null) return null; return valueForObject(selectedObject, aKey); } /** * Sets the specified value for the specified key on all selected objects. */ public boolean setSelectedObjectValue(Object aValue, String aKey) { Object selectedObject = selectedObject(); if (selectedObject == null) return false; return setValueForObject(aValue, selectedObject, aKey); } /** * Sets the specified value for the specified key on the specified object. * Validations may be triggered, and error dialogs may appear to the user. * * @return True if the value was set successfully, false if the value could not * be set and the update operation should not continue. */ public boolean setValueForObject(Object aValue, Object anObject, String aKey) { // notify object's observers: // this includes us, and will notify our observers EOObserverCenter.notifyObserversObjectWillChange(anObject); // TODO: if key is null, need to remove old object // and add new object instead of simply replacing it. try { if (anObject instanceof EOKeyValueCoding) { ((EOKeyValueCoding) anObject).takeValueForKey(aValue, aKey); } else { EOKeyValueCodingSupport.takeValueForKey(anObject, aValue, aKey); } } catch (RuntimeException exc) { Object result = notifyDelegate("displayGroupShouldDisplayAlert", new Class[] { WODisplayGroup.class, String.class, String.class }, new Object[] { this, "Error", exc.getMessage() }); if ((result == null) || (Boolean.TRUE.equals(result))) { throw exc; } return false; } notifyDelegate("displayGroupDidSetValueForObject", new Class[] { WODisplayGroup.class, Object.class, Object.class, String.class }, new Object[] { this, aValue, anObject, aKey }); return true; } /** * Calls setValueForObject() for the object at the specified index. */ public boolean setValueForObjectAtIndex(Object aValue, int anIndex, String aKey) { return setValueForObject(aValue, displayedObjects.objectAtIndex(anIndex), aKey); } /** * Returns the value for the specified key on the specified object. */ public Object valueForObject(Object anObject, String aKey) { // empty string is considered the identity property if (aKey == null) return anObject; if (aKey.equals("")) return anObject; try { if (anObject instanceof EOKeyValueCoding) { return ((EOKeyValueCoding) anObject).valueForKey(aKey); } else { return EOKeyValueCodingSupport.valueForKey(anObject, aKey); } } catch (RuntimeException exc) { Object result = notifyDelegate("displayGroupShouldDisplayAlert", new Class[] { WODisplayGroup.class, String.class, String.class }, new Object[] { this, "Error", exc.getMessage() }); if ((result == null) || (Boolean.TRUE.equals(result))) { throw exc; } return null; } } /** * Calls valueForObject() for the object at the specified index. Returns null if * out of bounds. */ public Object valueForObjectAtIndex(int anIndex, String aKey) { if (displayedObjects.count() <= anIndex) return null; Object o = displayedObjects.objectAtIndex(anIndex); return valueForObject(o, aKey); } /** * Prints out the list of displayed objects. */ public String toString() { return displayedObjects.toString(); } /** * Handles notifications from the data source's editing context, looking for * InvalidatedAllObjectsInStoreNotification and * ObjectsChangedInEditingContextNotification, refetching in the former case and * updating displayed objects in the latter. Note: This method is not in the * public specification. */ public void objectsInvalidatedInEditingContext(NSNotification aNotification) { if (EOObjectStore.InvalidatedAllObjectsInStoreNotification.equals(aNotification.name())) { Object result = notifyDelegate("displayGroupShouldRefetch", new Class[] { WODisplayGroup.class, NSNotification.class }, new Object[] { this, aNotification }); if ((result == null) || (Boolean.TRUE.equals(result))) { fetch(); } } else if (EOEditingContext.ObjectsChangedInEditingContextNotification.equals(aNotification.name())) { Object result = notifyDelegate("displayGroupShouldRedisplay", new Class[] { WODisplayGroup.class, NSNotification.class }, new Object[] { this, aNotification }); if ((result == null) || (Boolean.TRUE.equals(result))) { int index; Enumeration e; boolean didChange = false; NSDictionary userInfo = aNotification.userInfo(); // inserts are ignored // mark updated objects as updated NSArray updates = (NSArray) userInfo.objectForKey(EOObjectStore.UpdatedKey); e = updates.objectEnumerator(); while (e.hasMoreElements()) { index = indexOf(displayedObjects, e.nextElement()); if (index != NSArray.NotFound) { // System.out.println( "WODisplayGroup: updated: " + index ); if (!didChange) { didChange = true; willChange(); updatedObjectIndex = index; } else { updatedObjectIndex = -1; } } } // treat invalidated objects as updated NSArray invalidates = (NSArray) userInfo.objectForKey(EOObjectStore.InvalidatedKey); e = invalidates.objectEnumerator(); while (e.hasMoreElements()) { index = indexOf(displayedObjects, e.nextElement()); if (index != NSArray.NotFound) { // System.out.println( "WODisplayGroup: invalidated: " + index ); if (!didChange) { didChange = true; willChange(); updatedObjectIndex = index; } else { updatedObjectIndex = -1; } } } // remove deletes from display group if they exist NSArray deletes = (NSArray) userInfo.objectForKey(EOObjectStore.DeletedKey); e = deletes.objectEnumerator(); Object o; while (e.hasMoreElements()) { o = e.nextElement(); index = indexOf(displayedObjects, o); if (index != NSArray.NotFound) { // System.out.println( "WODisplayGroup: deleted: " + o ); deleteObjectAtIndexNoNotify(index); } } if (!usesOptimisticRefresh()) { updateDisplayedObjects(); } } } } // static methods /** * Specifies the default behavior for whether changes should be validated * immediately for all display groups. */ public static boolean globalDefaultForValidatesChangesImmediately() { return globalDefaultForValidatesChangesImmediately; } /** * Specifies the default string matching format for all display groups. */ public static String globalDefaultStringMatchFormat() { return globalDefaultStringMatchFormat; } /** * Specifies the default string matching operator for all display groups. */ public static String globalDefaultStringMatchOperator() { return globalDefaultStringMatchOperator; } /** * Sets the default behavior for validating changes for all display groups. */ public static void setGlobalDefaultForValidatesChangesImmediately(boolean validatesImmediately) { globalDefaultForValidatesChangesImmediately = validatesImmediately; } /** * Sets the default string matching format that will be used by all display * groups. */ public static void setGlobalDefaultStringMatchFormat(String aFormat) { globalDefaultStringMatchFormat = aFormat; } /** * Sets the default string matching operator that will be used by all display * groups. */ public static void setGlobalDefaultStringMatchOperator(String anOperator) { globalDefaultStringMatchOperator = anOperator; } /** * Needed because we don't inherit from NSObject. Calls * EOObserverCenter.notifyObserversObjectWillChange. */ protected void willChange() { EOObserverCenter.notifyObserversObjectWillChange(this); } /** * Returns the index of the specified object in the specified NSArray, comparing * by value or by reference as determined by the private instance variable * compareByReference. If not found, returns NSArray.NotFound. */ private int indexOf(NSArray anArray, Object anObject) { if (compareByReference) { return anArray.indexOfIdenticalObject(anObject); } else { return anArray.indexOf(anObject); } } // interface EOObserving /** * Receives notifications of changes from objects that are managed by this * display group. This implementation sets updatedObjectIndex as appropriate. */ public void objectWillChange(Object anObject) { int index = indexOf(displayedObjects, anObject); if (index != NSArray.NotFound) { updatedObjectIndex = index; willChange(); } } // interface EOEditingContext.Editor /** * Called before the editing context begins to save changes. This implementation * calls endEditing(). */ public void editingContextWillSaveChanges(EOEditingContext anEditingContext) { endEditing(); } /** * Called to determine whether this editor has changes that have not been * committed to the object in the context. This implementation returns false. */ public boolean editorHasChangesForEditingContext(EOEditingContext anEditingContext) { return false; } // interface EOEditingContext.MessageHandler /** * Called to display a message for an error that occurred in the specified * editing context. If the delegate allows, this implementation writes a message * to the standard output. Override to customize. */ public void editingContextPresentErrorMessage(EOEditingContext anEditingContext, String aMessage) { Object result = notifyDelegate("displayGroupShouldDisplayAlert", new Class[] { WODisplayGroup.class, String.class, String.class }, new Object[] { this, "Error", aMessage }); if ((result == null) || (Boolean.TRUE.equals(result))) { System.out.println(aMessage); } } /** * Called by the specified object store to determine whether fetching should * continue, where count is the current count and limit is the limit as * specified by the fetch specification. This implementation returns true. * Override to customize. */ public boolean editingContextShouldContinueFetching(EOEditingContext anEditingContext, int count, int limit, EOObjectStore anObjectStore) { return true; } /** * Sends the specified message to the delegate. Returns the return value of the * method, or null if no return value or no delegate or no implementation. */ private Object notifyDelegate(String aMethodName, Class[] types, Object[] params) { try { Object delegate = delegate(); if (delegate == null) return null; return NSSelector.invoke(aMethodName, types, delegate, params); } catch (NoSuchMethodException e) { // ignore: not implemented } catch (Exception exc) { // log to standard error System.err.println("Error while messaging delegate: " + delegate + " : " + aMethodName); exc.printStackTrace(); } return null; } /** * DisplayGroups can delegate important decisions to a Delegate. Note that * DisplayGroup doesn't require its delegates to implement this interface: * rather, this interface defines the methods that DisplayGroup will attempt to * invoke dynamically on its delegate. The delegate may choose to implement only * a subset of the methods on the interface. */ public interface Delegate { /** * Called when the specified data source fails to create an object for the * specified display group. */ void displayGroupCreateObjectFailed(WODisplayGroup aDisplayGroup, EODataSource aDataSource); /** * Called after the specified display group's data source is changed. */ void displayGroupDidChangeDataSource(WODisplayGroup aDisplayGroup); /** * Called after a change occurs in the specified display group's selected * objects. */ void displayGroupDidChangeSelectedObjects(WODisplayGroup aDisplayGroup); /** * Called after the specified display group's selection has changed. */ void displayGroupDidChangeSelection(WODisplayGroup aDisplayGroup); /** * Called after the specified display group has deleted the specified object. */ void displayGroupDidDeleteObject(WODisplayGroup aDisplayGroup, Object anObject); /** * Called after the specified display group has fetched the specified object * list. */ void displayGroupDidFetchObjects(WODisplayGroup aDisplayGroup, List anObjectList); /** * Called after the specified display group has inserted the specified object * into its internal object list. */ void displayGroupDidInsertObject(WODisplayGroup aDisplayGroup, Object anObject); /** * Called after the specified display group has set the specified value for the * specified object and key. */ void displayGroupDidSetValueForObject(WODisplayGroup aDisplayGroup, Object aValue, Object anObject, String aKey); /** * Called by the specified display group to determine what objects should be * displayed for the objects in the specified list. * * @return An NSArray containing the objects to be displayed for the objects in * the specified list. */ NSArray displayGroupDisplayArrayForObjects(WODisplayGroup aDisplayGroup, List aList); /** * Called by the specified display group before it attempts to change the * selection. * * @return True to allow the selection to change, false otherwise. */ boolean displayGroupShouldChangeSelection(WODisplayGroup aDisplayGroup, List aSelectionList); /** * Called by the specified display group before it attempts to delete the * specified object. * * @return True to allow the object to be deleted false to prevent the deletion. */ boolean displayGroupShouldDeleteObject(WODisplayGroup aDisplayGroup, Object anObject); /** * Called by the specified display group before it attempts display the * specified alert to the user. * * @return True to allow the message to be displayed, false if you want to * handle the alert yourself and suppress the display group's * notification. */ boolean displayGroupShouldDisplayAlert(WODisplayGroup aDisplayGroup, String aTitle, String aMessage); /** * Called by the specified display group before it attempts fetch objects. * * @return True to allow the fetch to take place, false to prevent the fetch. */ boolean displayGroupShouldFetch(WODisplayGroup aDisplayGroup); /** * Called by the specified display group before it attempts to insert the * specified object. * * @return True to allow the object to be inserted false to prevent the * insertion. */ boolean displayGroupShouldInsertObject(WODisplayGroup aDisplayGroup, Object anObject, int anIndex); /** * Called by the specified display group when it receives the specified * ObjectsChangedInEditingContextNotification. * * @return True to allow the display group to update the display (recommended), * false to prevent the update. */ boolean displayGroupShouldRedisplay(WODisplayGroup aDisplayGroup, NSNotification aNotification); /** * Called by the specified display group when it receives the specified * InvalidatedAllObjectsInStoreNotification. * * @return True to allow the display group to refetch (recommended), false to * prevent the refetch. */ boolean displayGroupShouldRefetch(WODisplayGroup aDisplayGroup, NSNotification aNotification); } } /* * $Log$ Revision 1.2 2006/02/19 01:44:02 cgruber Add xmlrpc files Remove jclark * and replace with dom4j and javax.xml.sax stuff Re-work dependencies and * imports so it all compiles. * * Revision 1.1 2006/02/16 13:22:22 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.6 2003/08/07 00:15:15 chochos general cleanup (mostly removing * unused imports) * * Revision 1.5 2003/01/22 23:01:06 mpowers Better handling for index out of * bounds. * * Revision 1.4 2003/01/21 22:27:02 mpowers Corrected context id usage. * Implemented backtracking. * * Revision 1.3 2003/01/21 17:54:01 mpowers Batch indices are one-based, not * zero-based. * * Revision 1.2 2003/01/21 14:38:42 mpowers Fixed batching. * * Revision 1.1 2003/01/18 23:30:42 mpowers WODisplayGroup now compiles. * * Revision 1.46 2002/10/24 21:15:36 mpowers New implementations of NSArray and * subclasses. * * Revision 1.45 2002/10/24 18:20:20 mpowers Because NSArray is read-only, we * are returning our internal representations to callers of allObjects(), * displayedObjects(), and selectedObjects(). * * Revision 1.44 2002/08/06 18:20:25 mpowers Now posting DisplayGroupWillFetch * notifications before fetch. Implemented support for usesOptimisticRefresh. No * longer supporting inserted/updated/deleted lists: not part of spec. * * Revision 1.43 2002/05/17 15:01:49 mpowers Implemented dynamic lookup of * delegate methods so delegates no longer need to implement the * DisplayGroup.Delegate interface. * * Revision 1.42 2002/03/26 21:46:06 mpowers Contributing EditingContext as a * java-friendly convenience. * * Revision 1.41 2002/03/11 03:17:56 mpowers Provided control point for * coalesced changes. * * Revision 1.40 2002/03/05 23:18:28 mpowers Added documentation. Added * isSelectionPaintedImmediate and isSelectionTracking attributes to * TableAssociation. Added getTableAssociation to TableColumnAssociation. * * Revision 1.39 2002/02/19 22:26:04 mpowers Implemented * EOEditingContext.MessageHandler support. * * Revision 1.38 2002/02/19 16:37:38 mpowers Implemented support for * EOEditingContext.Editor * * Revision 1.37 2001/12/11 22:17:48 mpowers Now properly handling exceptions in * valueForObject. No longer trying to retain selection based only on index. * * Revision 1.36 2001/11/08 21:42:00 mpowers Now we know what to do with * shouldRefetch and shouldRedisplay. * * Revision 1.35 2001/11/04 18:26:58 mpowers Fixed bug where exceptions were not * properly reported when updating a value and the display group did not have a * delegate. * * Revision 1.34 2001/11/02 20:59:36 mpowers Now correctly ensuring selected * objects are a subset of displayed objects. * * Revision 1.33 2001/10/30 22:56:45 mpowers Added support for EOQualifier. * * Revision 1.32 2001/10/23 22:27:53 mpowers Now running at * ObserverPrioritySixth. * * Revision 1.31 2001/10/23 18:45:05 mpowers Rolling back changes. * * Revision 1.28 2001/08/22 19:23:41 mpowers No longer asserting objects in all * objects list. * * Revision 1.27 2001/07/30 16:17:01 mpowers Minor code cleanup. * * Revision 1.26 2001/07/10 22:49:07 mpowers Fixed bug in optimization for * selectObjectsIdenticalTo (found by Dongzhi). * * Revision 1.25 2001/06/19 15:40:21 mpowers Now only changing the selection if * the new selection is different from the old. * * Revision 1.24 2001/05/24 17:36:15 mpowers Fixed problem with * selectedObjectsIdenticalTo: it was using compare by value instead of compare * by reference. * * Revision 1.23 2001/05/18 21:09:19 mpowers Now throwing exceptions if the * delegate cannot handle error from update. * * Revision 1.22 2001/05/14 15:26:12 mpowers Now checking for null delegate * before and after selection change. * * Revision 1.21 2001/05/08 18:47:34 mpowers Minor fixes for d3. * * Revision 1.20 2001/04/29 22:02:45 mpowers Work on id transposing between * editing contexts. * * Revision 1.19 2001/04/13 16:38:09 mpowers Alpha3 release. * * Revision 1.18 2001/04/03 20:36:01 mpowers Fixed * refaulting/reverting/invalidating to be self-consistent. * * Revision 1.17 2001/03/29 03:31:13 mpowers No longer using Introspector. * * Revision 1.16 2001/02/27 03:32:18 mpowers Implemented default values for new * objects. * * Revision 1.15 2001/02/27 02:11:17 mpowers Now throwing exception when cloning * fails. Removed debugging printlns. * * Revision 1.14 2001/02/26 22:41:51 mpowers Implemented null placeholder * classes. Duplicator now uses NSNull. No longer catching base exception class. * * Revision 1.13 2001/02/26 15:53:22 mpowers Fine-tuning notification firing. * Child display groups now update properly after parent save or invalidate. * * Revision 1.12 2001/02/22 20:55:06 mpowers Implemented notification handling. * * Revision 1.11 2001/02/21 20:40:42 mpowers setObjectArray now falls back to * index when trying to retain the same selection. * * Revision 1.10 2001/02/20 16:38:55 mpowers MasterDetailAssociations now * observe their controlled display group's objects for changes to that the * parent object will be marked as updated. Before, only inserts and deletes to * an object's items are registered. Also, moved ObservableArray to package * access. * * Revision 1.9 2001/02/17 17:23:49 mpowers More changes to support compiling * with jdk1.1 collections. * * Revision 1.8 2001/02/17 16:52:05 mpowers Changes in imports to support * building with jdk1.1 collections. * * Revision 1.7 2001/01/24 16:35:37 mpowers Improved documentation on * TreeAssociation. SortOrderings are now inherited from parent nodes. Updates * after sorting are still lost on TreeController. * * Revision 1.6 2001/01/24 14:23:05 mpowers Added support for OrderedDataSource. * * Revision 1.5 2001/01/12 17:21:37 mpowers Implicit creation of EOSortOrderings * now happens in setSortOrderings. * * Revision 1.4 2001/01/11 20:34:26 mpowers Implemented EOSortOrdering and added * support in framework. Added header-click to sort table columns. * * Revision 1.3 2001/01/10 22:49:44 mpowers Implemented similarly named * selection methods instead of throwing exceptions. * * Revision 1.2 2001/01/09 20:12:52 mpowers Moved inner classes to package * access. * * Revision 1.1.1.1 2000/12/21 15:48:20 mpowers Contributing wotonomy. * * Revision 1.21 2000/12/20 16:25:39 michael Added log to all files. * * Revision 1.20 2000/12/15 15:04:42 michael Added doc. * * Revision 1.19 2000/12/11 13:32:48 michael Finish the much better * TreeAssociation implementation. TreeAssociation now has no gui dependencies. * * Revision 1.18 2000/12/05 17:41:46 michael Broadcasts selection change after * delegate refuses selection change so the initiating association gets * refreshed. * */