/*==========================================================================*\ | $Id: WCTreeModel.java,v 1.4 2011/11/08 14:05:23 aallowat Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2011 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT 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 General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.ui; import java.util.HashMap; import java.util.Map; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSKeyValueCodingAdditions; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSSet; import er.extensions.eof.ERXQ; import er.extensions.eof.ERXS; //------------------------------------------------------------------------- /** * The {@code WCTreeModel} class represents the data model used by the * {@link WCTree} component. It is roughly a hierarchical analogue to * {@code WODisplayGroup} (but more closely related to Apple's * {@code NSTreeController} from Cocoa), providing functionality such as * maintaining the selection state and the sort ordering of the tree. * * @param <T> the type of elements contained in the tree model. There is no * requirement that the tree model be homogeneous, so {@code Object} is * an acceptable parameter to easily get a heterogeneous model; however, * users who extend this class will have to perform the necessary typecasts * to retrieve the children of those objects based on the actual type * * @author Tony Allevato * @author Last changed by $Author: aallowat $ * @version $Revision: 1.4 $, $Date: 2011/11/08 14:05:23 $ */ public abstract class WCTreeModel<T> implements NSKeyValueCodingAdditions { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Initializes a new instance of the {@code WCTreeModel} class. */ public WCTreeModel() { cachedArrangedChildren = new HashMap<T, NSArray<T>>(); selectedObjects = new NSMutableSet<T>(); sortOrderings = null; qualifier = null; } //~ Methods ............................................................... // ---------------------------------------------------------- /** * <p> * Gets the children of the specified object. * </p><p> * Clients must override this method to return the children in their * natural order, irrespective of any filtering or sorting that is set on * the model. To retrieve the array of sorted and filtered children (as * the {@code WCTree} component does when it displays the content), one * should call {@link #arrangedChildrenOfObject(T)} instead. * </p> * * @param object the object whose children should be retrieved, or null to * retrieve the root objects * @return the children of the object. If the object is a leaf, this * method may return either null or an empty array */ public abstract NSArray<T> childrenOfObject(T object); // ---------------------------------------------------------- /** * <p> * Gets a value indicating whether a node has children, after any qualifier * filtering has been applied. This method is used by the tree component to * determine if an expansion arrow should be displayed for a particular * item. * </p><p> * By default, this method simply calls {@link #arrangedChildrenOfObject(Object)} * and returns true if the number of children is greater than 0. In * situations where determining the existence of children can be much * faster than computing the children themselves, subclasses should * override this method to provide a faster implementation. * </p> * * @param object the object to check for children * @return true if the object has children, otherwise false */ public boolean objectHasArrangedChildren(T object) { NSArray<T> children = arrangedChildrenOfObject(object); return (children != null && children.count() > 0); } // ---------------------------------------------------------- public T childWithPathComponent(T object, String component) { return null; } // ---------------------------------------------------------- public String pathForObject(T object) { return null; } // ---------------------------------------------------------- /** * <p> * Gets the children of the specified object, filtered according to the * model's qualifier and sorted according to the model's sort orderings. * </p><p> * Clients should not need to override this method, but rather the * abstract {@link #childrenOfObject(T)} method instead. * </p> * * @param object the object whose children should be retrieved, or null to * retrieve the root items * @return the sorted and filtered array of children of the object */ public NSArray<T> arrangedChildrenOfObject(T object) { NSArray<T> arrangedChildren; if (cachedArrangedChildren.containsKey(object)) { arrangedChildren = cachedArrangedChildren.get(object); } else { arrangedChildren = childrenOfObject(object); if (arrangedChildren != null) { if (qualifier != null) { arrangedChildren = ERXQ.filtered( arrangedChildren, qualifier); } if (sortOrderings != null) { arrangedChildren = ERXS.sorted( arrangedChildren, sortOrderings); } } cachedArrangedChildren.put(object, arrangedChildren); } return arrangedChildren; } // ---------------------------------------------------------- /** * <p> * Use this method to trigger reordering of the model's contents. * </p><p> * This method is automatically called by other {@code WCTreeModel} * methods that would affect the ordering of the objects in the model, * such as {@link #setSortOrderings(NSArray)} and * {@link #setQualifier(EOQualifier)}. Clients should invoke it directly * if an external event would force the objects in the model to change. * </p> */ public void rearrangeObjects() { cachedArrangedChildren.clear(); } // ---------------------------------------------------------- /** * Gets the sort orderings associated with the model. * * @return an {@code NSArray} of sort orderings */ public NSArray<EOSortOrdering> sortOrderings() { return sortOrderings; } // ---------------------------------------------------------- /** * <p> * Sets the sort orderings associated with the model. * </p><p> * Since the same sort orderings are applied at every level of the tree, * using this property only makes sense if the tree contains homogeneous * data (or at least data that has a common set of key paths). * </p> * * @param orderings an {@code NSArray} of sort orderings */ public void setSortOrderings(NSArray<EOSortOrdering> orderings) { sortOrderings = orderings; rearrangeObjects(); } // ---------------------------------------------------------- /** * Gets the qualifier used to filter the items in the model. * * @return an {@code EOQualifier} used to filter the items in the model */ public EOQualifier qualifier() { return qualifier; } // ---------------------------------------------------------- /** * <p> * Sets the qualifier used to filter the items in the model. Only items * that satisfy the qualifier will be included in the array returned by * {@link #arrangedChildrenOfObject(T)}. * </p><p> * The {@link WCTree} component currently calls * {@link #arrangedChildrenOfObject(T)} in such a way that this qualifier * would be applied top-down. This means that child items that match the * qualifier will only appear if all of their ancestors do as well. It * may be desirable to change this behavior in the future, for example, by * displaying the search results as a flat list that traverses the entire * tree. * </p><p> * Since the same qualifier is applied at every level of the tree, using * this property only makes sense if the tree contains homogeneous data * (or at least data that has a common set of key paths). * </p> * * @param aQualifier an {@code EOQualifier} used to filter the items in * the model */ public void setQualifier(EOQualifier aQualifier) { qualifier = aQualifier; rearrangeObjects(); } // ---------------------------------------------------------- /** * <p> * Gets a unique identifier for the specified object. This identifier is * used to persist the expansion state of the tree in the user's * preferences. * </p><p> * By default, the method returns null, which prevents the expansion state * from being remember for the object; if you wish to use this tree model * with a tree that should remember its expansion state across sessions, * you must override this method and provide an appropriate implementation * (for example, a directory tree could use path segments; a tree * containing EOs could use a combination of the entity name and object * ID). * </p> * * @param object the object * @return a String representing the unique identifier for the object */ public String persistentIdOfObject(T object) { return null; } // ---------------------------------------------------------- /** * Gets the object in the model that is located at the specified index path, * starting from the root. * * @param indexPath the index path of the object to retrieve * @return the object located at the index path, or null if none was found */ public T objectAtIndexPath(WCIndexPath indexPath) { T object = null; for (int index : indexPath.indices()) { NSArray<? extends T> children = childrenOfObject(object); if (children != null && children.count() > 0 && index < children.count()) { object = children.objectAtIndex(index); } else { return null; } } return object; } // ---------------------------------------------------------- /** * Gets a value indicating whether or not the specified object is allowed * to be selected. By default, all items can be selected, but this method * can be overridden to limit this. For example, a file picker may only * allow choosing items that represent files and not directories. * * @param anObject the object * @return true if the object can be selected, otherwise false */ public boolean canSelectObject(T anObject) { return true; } // ---------------------------------------------------------- /** * Gets the currently selected objects in the model. * * @return an NSSet containing the currently selected objects */ public NSSet<T> selectedObjects() { return selectedObjects; } // ---------------------------------------------------------- /** * Changes the model's {@code selectedObjects} to a set containing only * {@code anObject}. * * @param anObject the object to select */ public void setSelectedObject(T anObject) { if (anObject != null && canSelectObject(anObject)) { selectedObjects = new NSMutableSet<T>(anObject); } else { selectedObjects.removeAllObjects(); } selectionDidChange(); } // ---------------------------------------------------------- /** * Changes the model's {@code selectedObjects} to a set containing only * {@code objects}. * * @param objects the objects to select */ public void setSelectedObjects(NSSet<T> objects) { selectedObjects = new NSMutableSet<T>(); boolean changed = false; for (T object : objects) { if (canSelectObject(object)) { selectedObjects.addObject(object); changed = true; } } if (changed) { selectionDidChange(); } } // ---------------------------------------------------------- /** * Adds {@code anObject} to the model's current selection. * * @param anObject the object to add to the selection */ public void selectObject(T anObject) { if (canSelectObject(anObject)) { selectedObjects.addObject(anObject); selectionDidChange(); } } // ---------------------------------------------------------- /** * Removes {@code anObject} from the model's current selection. * * @param anObject the object to remove from the selection */ public void deselectObject(T anObject) { selectedObjects.removeObject(anObject); selectionDidChange(); } // ---------------------------------------------------------- /** * Adds {@code objects} to the model's current selection. * * @param objects the object to add to the selection */ public void selectObjects(NSSet<T> objects) { boolean changed = false; for (T object : objects) { if (canSelectObject(object)) { selectedObjects.addObject(object); changed = true; } } if (changed) { selectionDidChange(); } } // ---------------------------------------------------------- /** * Removes {@code objects} from the model's current selection. * * @param objects the object to remove from the selection */ public void deselectObjects(NSSet<T> objects) { selectedObjects.subtractSet(objects); selectionDidChange(); } // ---------------------------------------------------------- /** * Clears the current selection in the model. */ public void clearSelection() { selectedObjects.removeAllObjects(); selectionDidChange(); } // ---------------------------------------------------------- /** * A hook that subclasses can use to be notified when the tree selection * changes. This should be used when it is necessary to keep other data * structures synchronized with the selection after a page submit. */ protected void selectionDidChange() { // Do nothing; subclasses can override. } //~ KVC support implementation ............................................ // ---------------------------------------------------------- public Object valueForKey(String key) { return NSKeyValueCoding.DefaultImplementation.valueForKey(this, key); } // ---------------------------------------------------------- public void takeValueForKey(Object value, String key) { NSKeyValueCoding.DefaultImplementation.takeValueForKey( this, value, key); } // ---------------------------------------------------------- public Object valueForKeyPath(String keyPath) { return NSKeyValueCodingAdditions.DefaultImplementation .valueForKeyPath(this, keyPath); } // ---------------------------------------------------------- public void takeValueForKeyPath(Object value, String keyPath) { NSKeyValueCodingAdditions.DefaultImplementation.takeValueForKeyPath( this, value, keyPath); } //~ Static/instance variables ............................................. /* The cache used to maintain the lists of arranged children for each node. */ private Map<T, NSArray<T>> cachedArrangedChildren; /* The currently selected objects in the tree model. */ private NSMutableSet<T> selectedObjects; /* The sort orderings that define how to sort the elements at each level of the tree. */ private NSArray<EOSortOrdering> sortOrderings; /* A qualifier used to filter the displayed items in the tree. */ private EOQualifier qualifier; }