/* 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.ui; import java.lang.ref.Reference; import java.lang.ref.WeakReference; 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 javax.swing.JOptionPane; import net.wotonomy.control.EODataSource; import net.wotonomy.control.EODelayedObserver; 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.foundation.NSArray; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSMutableArray; import net.wotonomy.foundation.NSNotification; import net.wotonomy.foundation.NSNotificationCenter; import net.wotonomy.foundation.NSSelector; import net.wotonomy.foundation.internal.Duplicator; import net.wotonomy.foundation.internal.WotonomyException; /** * EODisplayGroup provides an abstraction of a user interface, * comprising of an ordered collection of data objects, some * of which are displayed, and of those some are selected. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ */ public class EODisplayGroup extends Observable implements EOObserving, EOEditingContext.Editor { /** * 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 EOAssociation editingAssociation; 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 contentsChanged; boolean selectionChanged; int updatedObjectIndex; // this property is not in the spec private boolean compareByReference = false; private EOObserving lastGroupObserver; /** * Creates a new display group. */ public EODisplayGroup () { 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; editingAssociation = null; qualifier = null; localKeys = new NSArray(); // not implemented insertedObjectDefaultValues = new NSDictionary(); fetchesOnLoad = false; // not implemented selectsFirstObjectAfterFetch = false; usesOptimisticRefresh = false; inQueryMode = false; // not implemented contentsChanged = false; selectionChanged = false; updatedObjectIndex = -1; // create our private delayed observer lastGroupObserver = new LastGroupObserver( this ); EOObserverCenter.addObserver( lastGroupObserver, this ); } // 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; } // 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 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." ); } // interface to associations /** * Called by an association when it begins editing. */ public void associationDidBeginEditing ( EOAssociation anAssociation ) { // System.out.println( "EODisplayGroup.associationDidBeginEditing: " + anAssociation ); if ( dataSource != null ) { if ( dataSource.editingContext() != null ) { dataSource.editingContext().setMessageHandler( this ); } } editingAssociation = anAssociation; } /** * Called by an association when it is finished editing. */ public void associationDidEndEditing ( EOAssociation anAssociation ) { // System.out.println( "EODisplayGroup.associationDidEndEditing: " + anAssociation ); editingAssociation = null; } /** * Called by associations to determine whether the contents * of any objects have been changed. Returns true if the * contents have changed and not all observers have been * notified. */ public boolean contentsChanged () { return contentsChanged; } /** * Called by associations to determine whether the * selection has been changed. Returns true if the * selection has changed and not all observers have * been notified. */ public boolean selectionChanged () { return selectionChanged; } /** * Called by an association when a user-specified value fails the association's * validation rules. This implementation returns true, unless the delegate * prevents this. * @return True to allow the association to handle user notification, * otherwise return false to let the association know that the * display group notified the user. */ public boolean associationFailedToValidateValue ( EOAssociation anAssociation, String aValue, String aKey, Object anObject, String anErrorDescription ) { Object result = notifyDelegate( "displayGroupShouldDisplayAlert", new Class[] { EODisplayGroup.class, String.class, String.class }, new Object[] { this, "Validation Failed", anErrorDescription } ); if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) { return true; } return false; } /** * Called by an association to determine whether it should enable * a component that displayes a value for the specified key. * @return true if an object is selected, or if * the specified key is a query key. Otherwise false. */ public boolean enabledToSetSelectedObjectValueForKey ( String aKey ) { if ( ( aKey != null ) && ( aKey.startsWith( "@" ) ) ) return true; return ( selectedObject() != null ); } // association management /** * Returns the association that is currently being edited, * or null if no editing is taking place. */ public EOAssociation editingAssociation () { return editingAssociation; } /** * Asks the association currently editing to stop editing. * @returns true if editing was stopped, false if the * association refused to stop editing (if a modal dialog * is displayed or a value failed to validate). */ public boolean endEditing () { if ( editingAssociation == null ) return true; return editingAssociation.endEditing(); } /** * Returns a read-only List of associations that are observing * this display group. */ public NSArray observingAssociations () { NSArray observers = EOObserverCenter.observersForObject( this ); NSMutableArray result = new NSMutableArray(); Object o; Enumeration e = observers.objectEnumerator(); while ( e.hasMoreElements() ) { o = e.nextElement(); if ( o instanceof EOAssociation ) { result.addObject( o ); } } return result; } // 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; } /** * 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[] { EODisplayGroup.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[] { EODisplayGroup.class }, new Object[] { this } ); notifyDelegate( "displayGroupDidChangeSelectedObjects", new Class[] { EODisplayGroup.class }, new Object[] { this } ); return true; } /** * 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[] { EODisplayGroup.class, Object.class }, new Object[] { this, target } ); if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) { return false; } contentsChanged = true; deleteObjectAtIndexNoNotify( anIndex ); if ( dataSource != null ) { dataSource.deleteObject( target ); } notifyDelegate( "displayGroupDidDeleteObject", new Class[] { EODisplayGroup.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" ); return displayedObjectsProxy; } /** * 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. */ public boolean fetch () { endEditing(); if ( dataSource == null ) { return false; } Object result = notifyDelegate( "displayGroupShouldFetch", new Class[] { EODisplayGroup.class }, new Object[] { this } ); if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) { return false; } NSNotificationCenter.defaultCenter().postNotification( DisplayGroupWillFetchNotification, this, new NSDictionary() ); NSArray objectList = dataSource.fetchObjects(); notifyDelegate( "displayGroupDidFetchObjects", new Class[] { EODisplayGroup.class, List.class }, new Object[] { this, objectList } ); if ( selectsFirstObjectAfterFetch ) { //note: there's a good chance this logic ought to be in master-detail assoc: // we're doing this because changes in the master object trigger a refetch // on the child display group which annoyingly changes the selection. NSArray original = new NSArray( allObjects ); setObjectArray( objectList ); if ( displayedObjects.size() > 0 && !original.equals( allObjects ) ) // don't change if no change { setSelectionIndexes( new NSArray( new Integer( 0 ) ) ); } } else { setObjectArray( objectList ); } return true; } /** * Creates a new object at the specified index. * Calls insertObjectAtIndex() with the result * from sending createObject() to the data source. * Presents a JOptionPane if the create fails, unless * the delegate implements displayGroupCreateObjectFailed. * @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[] { EODisplayGroup.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(); } } } // no delegate or delegate does not implement displayGroupCreateObjectFailed String message = "Data source could not create new object"; Object delegateResult = notifyDelegate( "displayGroupShouldDisplayAlert", new Class[] { EODisplayGroup.class, String.class, String.class }, new Object[] { this, "Error", message } ); if ( ( delegateResult == null ) || ( Boolean.TRUE.equals( delegateResult ) ) ) { JOptionPane.showMessageDialog( null, message ); } } 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[] { EODisplayGroup.class, Object.class, int.class }, new Object[] { this, anObject, new Integer(anIndex) } ); if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) { return; } contentsChanged = true; updatedObjectIndex = anIndex; willChange(); // 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[] { EODisplayGroup.class, Object.class }, new Object[] { this, anObject } ); } /** * Sets contentsChanged to true and notifies all observers. */ public void redisplay () { contentsChanged = true; 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 true if an object was selected. */ public boolean selectNext () { int count = displayedObjects.count(); if ( count == 0 ) return false; if ( count == 1 ) { selectObject( displayedObjects.objectAtIndex( 0 ) ); return true; } 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 ); } return selectObject( selectedObject ); } /** * 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[] { EODisplayGroup.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[] { EODisplayGroup.class }, new Object[] { this } ); notifyDelegate( "displayGroupDidChangeSelectedObjects", new Class[] { EODisplayGroup.class }, new Object[] { this } ); return true; } /** * 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 true if an object was selected. */ public boolean selectPrevious () { int i = displayedObjects.count(); if ( i == 0 ) return false; if ( i == 1 ) { selectObject( displayedObjects.objectAtIndex( 0 ) ); return true; } 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; } return selectObject( displayedObjects.objectAtIndex( i ) ); } /** * 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[] { EODisplayGroup.class, List.class }, new Object[] { this, anObjectList } ); if ( result != null ) { anObjectList = (List) result; } contentsChanged = true; 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 ); } /** * 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 () { contentsChanged = true; 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[] { EODisplayGroup.class }, new Object[] { this } ); notifyDelegate( "displayGroupDidChangeSelectedObjects", new Class[] { EODisplayGroup.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[] { EODisplayGroup.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[] { EODisplayGroup.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[] { EODisplayGroup.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. */ public Object valueForObjectAtIndex ( int anIndex, String aKey ) { 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[] { EODisplayGroup.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[] { EODisplayGroup.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( "EODisplayGroup: updated: " + index ); if ( ! didChange ) { didChange = true; contentsChanged = 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( "EODisplayGroup: invalidated: " + index ); if ( ! didChange ) { didChange = true; contentsChanged = 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( "EODisplayGroup: 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 ); } /** * Called by LastGroupObserver to clear flags. */ protected void processRecentChanges() { contentsChanged = false; selectionChanged = false; } /** * 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 and contentsChanged as appropriate. */ public void objectWillChange(Object anObject) { int index = indexOf( displayedObjects, anObject ); if ( index != NSArray.NotFound ) { updatedObjectIndex = index; contentsChanged = true; 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. */ public boolean editorHasChangesForEditingContext( EOEditingContext anEditingContext ) { return ( editingAssociation() != null ); } // interface EOEditingContext.MessageHandler /** * Called to display a message for an error that occurred * in the specified editing context. If the delegate allows, * this implementation presents an informational JOptionPane. * Override to customize. */ public void editingContextPresentErrorMessage( EOEditingContext anEditingContext, String aMessage ) { Object result = notifyDelegate( "displayGroupShouldDisplayAlert", new Class[] { EODisplayGroup.class, String.class, String.class }, new Object[] { this, "Error", aMessage } ); if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) { JOptionPane.showMessageDialog( null, 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 presents an JOptionPane allowing the user * to specify whether to continue. Override to customize. */ public boolean editingContextShouldContinueFetching( EOEditingContext anEditingContext, int count, int limit, EOObjectStore anObjectStore ) { return ( JOptionPane.showConfirmDialog( null, "Fetch limit reached: do you wish to continue?", "Continue?", JOptionPane.YES_NO_OPTION ) == JOptionPane.YES_OPTION ); } /** * 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 ( EODisplayGroup aDisplayGroup, EODataSource aDataSource ); /** * Called after the specified display group's * data source is changed. */ void displayGroupDidChangeDataSource ( EODisplayGroup aDisplayGroup ); /** * Called after a change occurs in the specified * display group's selected objects. */ void displayGroupDidChangeSelectedObjects ( EODisplayGroup aDisplayGroup ); /** * Called after the specified display group's * selection has changed. */ void displayGroupDidChangeSelection ( EODisplayGroup aDisplayGroup ); /** * Called after the specified display group has * deleted the specified object. */ void displayGroupDidDeleteObject ( EODisplayGroup aDisplayGroup, Object anObject ); /** * Called after the specified display group * has fetched the specified object list. */ void displayGroupDidFetchObjects ( EODisplayGroup aDisplayGroup, List anObjectList ); /** * Called after the specified display group * has inserted the specified object into * its internal object list. */ void displayGroupDidInsertObject ( EODisplayGroup aDisplayGroup, Object anObject ); /** * Called after the specified display group * has set the specified value for the specified * object and key. */ void displayGroupDidSetValueForObject ( EODisplayGroup 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 ( EODisplayGroup 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 ( EODisplayGroup 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 ( EODisplayGroup 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 ( EODisplayGroup 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 ( EODisplayGroup 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 ( EODisplayGroup 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 ( EODisplayGroup 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 ( EODisplayGroup aDisplayGroup, NSNotification aNotification ); } } /** * A private class that will serve to clear the contentsChanged * and selectionChanged flags after all Associations have been * notified. */ class LastGroupObserver extends EODelayedObserver { Reference ref; public LastGroupObserver( EODisplayGroup aDisplayGroup ) { ref = new WeakReference( aDisplayGroup ); } /** * We want to be informed last, after all Associations * have been notified to changes in the DisplayGroup. */ public int priority() { return ObserverPrioritySixth; } /** * After all Associations have been notified, * clear the contentsChanged and selectionChanged flags. */ public void subjectChanged () { EODisplayGroup group = (EODisplayGroup) ref.get(); if ( group != null ) { group.processRecentChanges(); } } } /* * $Log$ * Revision 1.2 2006/02/18 23:14:35 cgruber * Update imports and maven dependencies. * * Revision 1.1 2006/02/16 13:22:22 cgruber * Check in all sources in eclipse-friendly maven-enabled packages. * * Revision 1.51 2004/01/28 18:35:40 mpowers * Slight optimization: only comparing list in fetch if we have to. * * Revision 1.50 2004/01/27 20:42:30 mpowers * No longer reselecting first after fetch if contents are identical. * * Revision 1.49 2003/12/18 11:37:45 mpowers * Now calling qualifier internally. * * Revision 1.48 2003/08/06 23:07:52 chochos * general code cleanup (mostly, removing unused imports) * * Revision 1.47 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. * */