/* 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. * */