/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import tufts.Util; import static tufts.Util.copy; import java.util.*; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Color; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; /** * Manage a group of children within a parent. * * Handle rendering, duplication, adding/removing and reordering (z-order) of children. * * @version $Revision: 1.170 $ / $Date: 2010-05-21 19:35:54 $ / $Author: brian $ * @author Scott Fraize */ public abstract class LWContainer extends LWComponent { protected static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWContainer.class); private static final Object REMOVE_DEFAULT = "default"; private static final Object REMOVE_DELETE = "delete"; protected java.util.List<LWComponent> mChildren = NO_CHILDREN; @Override public void XML_fieldAdded(Object context, String name, Object child) { super.XML_fieldAdded(context, name, child); if (child instanceof LWComponent) { ((LWComponent)child).setParent(this); } } /* * Child handling code */ @Override public boolean hasChildren() { return mChildren.size() > 0; } @Override public int numChildren() { return mChildren.size(); } /** @return true: default allows children dragged in and out */ @Override public boolean supportsChildren() { return true; } /** @return true by default */ @Override public boolean supportsSlide() { return true; } @Override public boolean hasChild(LWComponent c) { return mChildren.contains(c); } /** @return true if we have any children */ @Override public boolean hasContent() { return mChildren.size() > 0; } /** * @return the list of children. Note that during a restore, this will always return a list so * that castor can add children to it. This should be the only time the returned list should * be modified -- callers of this for iteration should NOT modify the list contents. */ public java.util.List<LWComponent> getXMLChildList() { if (mXMLRestoreUnderway) { if (mChildren == NO_CHILDREN) mChildren = new ArrayList(); return mChildren; } else return mChildren; } /** * @return the list of children, or NO_CHILDREN if we've never had any children * The collection returned is guaranteed to iterate in the z-order of the children (top most is last). */ @Override public java.util.List<LWComponent> getChildren() { return mChildren; } /** @deprecated - use getChildren */ @Override public java.util.List<LWComponent> getChildList() { return getChildren(); } /** return child at given index, or null if none at that index */ @Override public LWComponent getChild(int index) { if (mChildren.size() > index) return mChildren.get(index); else return null; } public Iterator getChildIterator() { return mChildren == NO_CHILDREN ? Util.EmptyIterator : mChildren.iterator(); } /** * Layout this node as well as all descendents. This is sure to use Order.DEPTH to layout the * deepeset children first (so children will already be sized propertly out when parents * attempt to lay them out). */ @Override public void layoutAll(Object triggerKey) { // We could skip the list fetch by making this a recursive descent call, tho, for the LWMap // level, we want to use ChildKind.ANY to hit EVERYTHING, inckude slides on pathways, // master slides, etc. for (LWComponent c : getAllDescendents(ChildKind.ANY, new ArrayList(), Order.DEPTH)) c.layout(triggerKey); // All descendents laid out -- now lay this component out. super.layout(triggerKey); } /** In case the container subclass can do anything to lay out it's children * (e.g., so that LWNode can override & handle chil layout). */ void layoutChildren() { } @Override protected void notifyMapLocationChanged(LWComponent src, double mdx, double mdy) { super.notifyMapLocationChanged(src, mdx, mdy); if (hasChildren()) { for (LWComponent c : getChildren()) c.notifyMapLocationChanged(src, mdx, mdy); // is overcalling updateConnectedLinks (+1 for each depth!), but it's cheap } } @Override protected void updateConnectedLinks(LWComponent movingSrc) { super.updateConnectedLinks(movingSrc); if (updatingLinks()) { // these components are moving in absolute map coordinates, even tho their local location isn't changing for (LWComponent c : getChildren()) c.updateConnectedLinks(movingSrc); } } /** called by LWChangeSupport, available here for override by parent classes that want to * monitor what's going on with their children */ void broadcastChildEvent(LWCEvent e) { notifyLWCListeners(e); } /** * @param possibleChildren should contain at least one child of this container * to be reparented. Children not in this container are ignored. * @param newParent is the new parent for any children of ours found in possibleChildren */ public void reparentTo(final LWContainer newParent, Collection<LWComponent> possibleChildren) { if (newParent == null) { Log.warn(this + "; reparentTo: null new parent reparenting " + possibleChildren); return; } if (newParent == this) { //Util.printStackTrace(this + "; attempting to reparent back to ourself: " + possibleChildren); Log.warn(this + "; reparentTo: attempting to reparent back to ourself: " + possibleChildren); return; } notify(LWKey.HierarchyChanging); final List<LWComponent> reparenting = new ArrayList(); for (LWComponent c : possibleChildren) { if (c.getParent() == this) reparenting.add(c); } removeChildren(reparenting); /* for (LWComponent c : reparenting) { float x = c.getX(); float y = c.getY(); c.setLocation(getMapX() + x, getMapY() + y); } */ newParent.addChildren(reparenting); } final void removeChild(LWComponent c) { removeChildren(Util.iterable(c)); } /** we're moving an entire list of children from one container to another -- preseve z-order, and prevent concurrent modication exceptions */ public void takeAllChildren(LWContainer source) { if (source == this) throw new IllegalArgumentException("takeAllChildren: source is target: " + this); final List<LWComponent> taking = copy(source.getChildren()); // we can't add the instance of the child list directly, as it's going to be // modified as it's iterated by addChildImpl, which is going to extract it from // from the source parent's list as each child is moved over source.removeChildren(taking); this.addChildren(taking, ADD_PRESORTED); //addChildren(taking.toArray(new LWComponent[taking.size()])); } /** Add the given LWComponents to us as children, using the order they appear in the array * for child order (z-order and/or visual order, depending on component impl) */ public void addChildren(LWComponent[] toAdd) { addChildren(Arrays.asList(toAdd), ADD_PRESORTED); } // protected List<LWComponent> sortToMeaningfulZOrder(List<LWComponent> toAdd) // { // // Do what we can to preserve any meaninful order already // // present in the new incoming children. // // If we're a group or a layer, use the ZOrderSorter if we can for the add order, // // otherwise, everything else uses the YSorter (e.g., a standard VUE node, // // which stacks it's children, will then display them in the same vertical // // order they had on the map). // LWComponent[] sorted = null; // // todo: the default should be z-order sorting: LWNode can override // // to use the YSorter // if (this instanceof LWGroup || this instanceof LWMap.Layer) { // //if (sorted[0].getParent() == null) { // if (toAdd.get(0).getParent() == null) { // // if first item in list has no parent, we assume none do (they're // // system drag or paste orphans), and we can't sort them based // // on layer, so we leave them alone for now until all nodes have a z-order // // value. // } else { // sorted = sort(toAdd, ZOrderSorter); // } // } else { // sorted = sort(toAdd, LWComponent.YSorter); // } // if (sorted == null) // return toAdd; // else // return Arrays.asList(sorted); // } //protected List<? extends LWComponent> sortForIncomingZOrder(Collection<? extends LWComponent> toAdd) protected Collection<? extends LWComponent> sortForIncomingZOrder(Collection<? extends LWComponent> toAdd) { // Do what we can to preserve any meaninful order already // present in the new incoming children. // the default is to replicate the existing z-order sort if (toAdd.size() == 0) return Collections.EMPTY_LIST; Collection<? extends LWComponent> sorted; if (Util.getFirst(toAdd).getParent() == null) { // if first item in list has no parent, we assume none do (they're // probably system drag or paste orphans), and we can't sort them based // on z-order, so we leave them alone for now until all nodes have a z-order // value. sorted = toAdd; } else { sorted = Arrays.asList(sort(toAdd, ZOrderSorter)); } // if (toAdd.get(0).getParent() == null) { // // if first item in list has no parent, we assume none do (they're // // probably system drag or paste orphans), and we can't sort them based // // on z-order, so we leave them alone for now until all nodes have a z-order // // value. // } else { // toAdd = Arrays.asList(sort(toAdd, ZOrderSorter)); // } return sorted; } // /** can only be called on constructing containers, and as long as all being added are either also out-model, // * or are about to be removed from the model (deleted) // */ // public void addAll(List<LWComponent> toAdd) { // if (getParent() != null) // throw new IllegalStateException(this + " can only add directly on out-model constructing LWContainers"); // if (mChildren == NO_CHILDREN) // mChildren = new ArrayList(toAdd.size()); // mChildren.addAll(toAdd); // } // /** absoulute minimum done to reparent -- for layers, where coordinate systems are fully registered */ // public void setChildren(final ArrayList<LWComponent> children) { // if (getParent() != null) // notify(LWKey.HierarchyChanging); // if (children == null) { // mChildren = NO_CHILDREN; // } else { // mChildren = children; // for (LWComponent c : children) { // if (c.parent instanceof LWMap.Layer) // c.parent = this; // else // Log.error("setChildren: cannot steal from a non-layer: " + c + "; in: " + c.parent); // } // } // } public void addOnTop(LWComponent existingChild, LWComponent newChild) { final int index = indexOf(existingChild); final Object context; if (index < 0) { Log.warn("not an existing child: " + existingChild); context = ADD_DEFAULT; } else { context = Integer.valueOf(index + 1); } addChildren(Collections.singletonList(newChild), context); } // final void addChildren(Collection<LWComponent> toAdd, Object context) { // if (toAdd instanceof List) { // addChildren((List) toAdd, context); // } else { // addChildren(new ArrayList(toAdd), context); // } // } /** * This will add the contents of the iterable as children to the LWContainer. If * the iterable is a Collection of size greater than 1, we will sort the add list * by Y value first to preserve any visual ordering that may be present as best * we can (e.g., a vertical arrangement of nodes on a map, if dropped into a node, * should keep that order in the node's vertical layout. So we apply the * on-map Y-ordering to the natural order). * Special case: if we're a Group, we sort by z-order to preserve visual layer. */ @Override public void addChildren(Collection<? extends LWComponent> toAdd, Object context) { if (DEBUG.PARENTING) track("addChildren/"+context, toAdd); //if (toAdd.size() == 1) Util.printStackTrace("ADDONE"); if (toAdd == null || toAdd.size() < 1) return; notify(LWKey.HierarchyChanging); if (mChildren == NO_CHILDREN) mChildren = new ArrayList(toAdd.size()); if (toAdd.size() > 1 && context != ADD_PRESORTED) toAdd = sortForIncomingZOrder(toAdd); // in case the passed in toAdd is a list being used elsewhere, we create a copy // to use in the undo queue (and it's also possible that an individual call to // addChildImpl will fail) Note: the added list is for components that may be // trying to track what's going on in the model: the UndoManager handles // hierarchy changes is it's own internal, very reliable way: saving the entire // list of children the first time a HierarchyChanging event is seen during a // user action, and after that, it can ignore further changes on the same // parent: it has all it needs to restore the parent's state before the action. final List<LWComponent> added = new ArrayList(toAdd.size()); final long timestamp = System.currentTimeMillis(); for (LWComponent c : toAdd) { // if (c.getParent() == this) // currently needed to support re-dropping into an LWNode // continue; try { // although a time-stamp will be set if one isn't already provided via // the call to ensureID in addChildImpl, we do this here so that add // events that involve a collection of nodes will provide the exact same // timestamp for all the brand new nodes in the collection if (c.getCreated() == 0) c.setCreated(timestamp); addChildImpl(c, context); added.add(c); } catch (Throwable t) { Log.error(this + "; addChildImpl: " + c + ";", t); } } notify(LWKey.ChildrenAdded, added); // todo: for consistency, we should also be issuing a general HierarchyChanged // event. Note we'll have to check into how this effects the UndoManager // implemention. Also: consider an impl that gets rid of // ChildrenAdded/ChildrenRemoved completely, and handles it via an internal // setChildren that just issues a HierarchyChanged event (simlar to the design // of LWPathway.setEntries) layout(); } // @Override // public void notifyHierarchyChanging() { // super.notifyHierarchyChanging(); // for (LWComponent c : getChildren()) // c.notifyHierarchyChanging(); // } @Override public void notifyHierarchyChanged() { super.notifyHierarchyChanged(); for (LWComponent c : getChildren()) c.notifyHierarchyChanged(); } private void track(String where, Object o) { if (DEBUG.Enabled) Log.debug(String.format("\"%-8.8s\" %23s: %s", getDisplayLabel(), where, o instanceof LWComponent ? o : Util.tags(o))); } // TODO: deleteAll: can removeChildren on all, then remove all from model protected void addChildImpl(LWComponent c, Object context) { if (DEBUG.PARENTING) track("addChildImpl/"+context, c); //new Throwable("ADDCHILDIMPL").printStackTrace(); if (c == this) throw new Error("attempt to add self as child"); if (context == ADD_MERGE && c.hasAncestor(this)) return; if (c.getParent() != null && c.getParent().hasChild(c)) { //if (DEBUG.PARENTING) System.out.println("["+getLabel() + "] auto-deparenting " + c + " from " + c.getParent()); if (DEBUG.PARENTING) //if (DEBUG.META) tufts.Util.printStackTrace("FYI["+getLabel() + "] auto-deparenting " + c + " from " + c.getParent()); else track("addChildImpl/steal from", c.getParent()); //out("auto-deparenting " + c + " from " + c.getParent()); if (c.getParent() == this) { //if (DEBUG.PARENTING) System.out.println("["+getLabel() + "] ADD-BACK " + c + " (already our child)"); if (DEBUG.PARENTING) out("ADD-BACK " + c + " (already our child)"); // this okay -- in fact useful for child node re-drop on existing parent to trigger // re-ordering & re-layout } //c.notifyHierarchyChanging(); c.getParent().removeChild(c); // is LWGroup requesting cleanup??? } //if (c.getFont() == null)//todo: really want to do this? only if not manually set? // c.setFont(getFont()); if (mChildren == NO_CHILDREN) mChildren = new ArrayList(); if (context.getClass() == Integer.class) { // if context is an Integer, it means a specific index to add at mChildren.add( (Integer)context, c); } else { mChildren.add(c); } // consider a real notifyAdd, or notifyAdding/notifyAdded //---------------------------------------------------------------------------------------- // Delicately reparent, taking care that the model does not generate events while // in an indeterminate state. //---------------------------------------------------------------------------------------- setAsChildAndLocalize(c); // final double newParentMapScale = getMapScale(); // if (oldParentMapScale != newParentMapScale) { // notifyMapScaleChanged(oldParentMapScale, newParentMapScale); // } //c.reparentNotify(this); ensureID(c); c.notifyHierarchyChanged(); } protected void setAsChildAndLocalize(LWComponent c) { final LWContainer oldParent = c.getParent(); //---------------------------------------------------------------------------------------- // Delicately reparent, taking care that the model does not generate events while // in an indeterminate state. //---------------------------------------------------------------------------------------- if (oldParent != null && !isManagingChildLocations()) { // If we're managing child locations (e.g., and LWNode), no need // to localize, as the node will have it's position explicitly assigned // by the parent when it lays itself out // Save current mapX / mapY before setting the parent (which would change the reported mapX / mapY) final float oldMapX = c.getMapX(); final float oldMapY = c.getMapY(); final double oldParentMapScale = c.getMapScale(); if (false) { localizeCoordinates(c, oldParent, oldParentMapScale, oldMapX, oldMapY); c.setParent(this); } else { // Now set the parent, so that when the new location is set, it's already in it's // new parent, and it's mapX / mapY will report correctly when asked (e.g., the // bounds are immediatley correct for anyone listening to the location event). c.setParent(this); try { localizeCoordinates(c, oldParent, oldParentMapScale, oldMapX, oldMapY); } catch (Throwable t) { Util.printStackTrace(t); } } } else { c.setParent(this); } } // @Override // protected void notifyMapScaleChanged(double oldParentMapScale, double newParentMapScale) { // for (LWComponent c : getChildList()) { // c.notifyMapScaleChanged(oldParentMapScale, newParentMapScale); // } // } protected void localizeCoordinates(LWComponent c, LWContainer oldParent, double oldParentMapScale, float oldMapX, float oldMapY) { // Even if the new parent is managing the location, and is about to re-layout and set a new // location for this component, we first need to make sure it's location is at least within // the coordinate space of it's new parent (it's location is relative to it's new parent), // so that when the generic setLocation later runs, it can accuratly compute it's x-map // delta-x and delta-y, and make mapLocationChanged calls to update descendents. If it's // old location at that point relative to another, unknown parent, we'd have no way of // actually knowing it's old absolute map location. // This can be a problem during new object creation (e.g., LWGroups) -- if the new object // is grabbing children from the map, but the new object is under construction and is not // ON the map yet, these setLocation's will never make it do the undo manager to be undone // later: This is why we've added the special setLocation API with an event source. if (getParent() == null) { //if (DEBUG.Enabled) Util.printStackTrace("skipping coordinate localization in unparented (new?) LWContainer: " + this + "; for " + c); if (DEBUG.Enabled) Log.warn("coordinate localization in unparented (new?) LWContainer: " + this + "; for " + c); } //else if (DEBUG.PARENTING || DEBUG.CONTAINMENT) out("localizing coordinates: " + c + " oldParent=" + oldParent); final LWComponent eventSource; // c.getMap() should == getMap() at this point; setParent to this LWContainer has been done above if (c.getMap() != getMap()) Util.printStackTrace("different maps? " + c.getMap() + " != " + getMap()); if (c.getID() != null && c.getMap() == null) { // if ID is null, the object is still being created (and we don't need to worry about undoing it's initializations) if (oldParent == null || oldParent.getMap() == null) { if (DEBUG.Enabled) Util.printStackTrace("FYI: no event source for: " + c + ";\n\t localizing new parent: " + this + ";\n\toldParent is not in model either: " + oldParent + ";\n\tlocation events will not be available for UNDO"); eventSource = c; } else eventSource = oldParent; // if the component has no map, it's not in the model yet, and nobody can hear it. } else eventSource = c; // The version of LWComponent.setLocation with an eventSource argument was created // specifically for this call right here: final double scale = getMapScale(); c.setLocation((float) ((oldMapX - getMapX()) / scale), (float) ((oldMapY - getMapY()) / scale), eventSource, false); //oldParent == null); // The last param above if true means make calls to mapLocationChanged. // We'll need something to handle this for LWlinks when dropped into a // scaled on-map LWSlide (future feature), tho maybe that'll be handled by a // scaleNotify. This works right now because normally you can only drop // into a node, in which case it's layout code will make another setLocation // call that the link can pick up, or something at 100% scale, in which case // when the drop happens, the curved link's control points are already // exactly where they need to be, right where they are, as set by the // dragging code. if (DEBUG.Enabled||DEBUG.PARENTING || DEBUG.CONTAINMENT) out(" now localized: " + c); } /** * Remove any children in this iterator from this container. */ protected final void removeChildren(Iterable<LWComponent> iterable) { removeChildren(iterable, REMOVE_DEFAULT); } //private void removeChildren(Iterable<LWComponent> iterable, boolean permanent) /** * Remove any children in this iterator from this container. * If context is REMOVE_DELETE, just ignore objects that are not currently our children. */ protected void removeChildren(Iterable<LWComponent> iterable, Object context) { final boolean permanent = (context == REMOVE_DELETE); notify(LWKey.HierarchyChanging); ArrayList removedChildren = new ArrayList(); for (LWComponent c : iterable) { if (c.getParent() == this) { //c.notifyHierarchyChanging(); removeChildImpl(c); removedChildren.add(c); if (permanent) c.removeFromModel(); } else if (!permanent) { Log.error(this + " asked to de-parent child it doesn't own: " + c); } } if (removedChildren.size() > 0) { notify(LWKey.ChildrenRemoved, removedChildren); layout(); } // todo: for consistency, we should also be issuing a general HierarchyChanged event. } public final void deleteChildrenPermanently(Iterable<LWComponent> iterable) { removeChildren(iterable, REMOVE_DELETE); } protected void removeChildImpl(LWComponent c) { //if (DEBUG.PARENTING) System.out.println("["+getLabel() + "] REMOVING " + c); if (DEBUG.PARENTING) track("removing", c); if (mChildren == NO_CHILDREN) { Util.printStackTrace(this + "; null child list is null trying to remove: " + c); return; } if (isDeleted()) { // just in case: new Throwable(this + " FYI: ZOMBIE PARENT DELETING CHILD " + c).printStackTrace(); return; } if (!mChildren.remove(c)) { Log.warn(this + "; didn't contain child for removal: " + c); /* if (DEBUG.PARENTING) { System.out.println(this + " FYI: didn't contain child for removal: " + c); if (DEBUG.META) new Throwable().printStackTrace(); } */ } //c.setParent(null); } /** * Delete a child and PERMANENTLY remove it from the model. * Differs from removeChild / removeChildren, which just * de-parent the nodes, tho leave any listeners & links to it in place. */ public final void deleteChildPermanently(LWComponent c) { removeChildren(Util.iterable(c), REMOVE_DELETE); } // public final void deleteChildPermanently(LWComponent c) // { // // if (c.isLocked()) { // // if (DEBUG.Enabled) out("is locked; deletion not permitted: " + c); // // return; // // } // if (DEBUG.UNDO || DEBUG.PARENTING) System.out.println("["+getLabel() + "] DELETING PERMANENTLY " + c); // // We did the "deleting" notification first, so anybody listening can still see // // the node in it's full current state before anything changes. But children // // now keep their parent reference until they're removed from the model, so the // // only thing different when removeFromModel issues it's LWKey.Deleting event is // // the parent won't list it as a child, but since it still has the parent ref, // // event up-notification will still work, which is good enough. (It's probably // // not safe to deliver more than one LWKey.Deleting event -- if need to put it // // back here, have to be able to tell removeFromModel optionally not to issue // // the event). // //c.notify(LWKey.Deleting); // removeChild(c); // c.removeFromModel(); // } protected void removeChildrenFromModel() { // in certian cases, removing an object from the model will trigger the // auto-deleting of other objects from the model (possible also children of // ours), so we iterate over a copy of the the list to ensure we don't get any // ConcurrentModificationException's for (LWComponent c : copy(getChildren())) c.removeFromModel(); } @Override protected void prepareToRemoveFromModel() { removeChildrenFromModel(); if (mChildren == VUE.ModelSelection) // todo: tmp debug throw new IllegalStateException("attempted to delete selection"); } @Override protected void restoreToModel() { for (LWComponent c : getChildren()) { c.setFlag(Flag.UNDELETING); c.setParent(this); c.restoreToModel(); // todo: take parent as argument } super.restoreToModel(); } // todo: should probably just get rid of this helper -- not worth bother // If keep, this code may belong on the node as it only implies to // the embedded nature of child components in nodes. private void ensureLinksPaintOnTopOfAllParents() { ensureLinksPaintOnTopOfAllParents((LWComponent) this); for (LWComponent c : getChildren()) { ensureLinksPaintOnTopOfAllParents(c); if (c instanceof LWContainer) ensureLinksPaintOnTopOfAllParents((LWContainer)c); } } private static void ensureLinksPaintOnTopOfAllParents(LWComponent component) { for (LWLink link : component.getLinks()) ensureLinkPaintsOverAllAncestors(link, component); } static void ensureLinkPaintsOverAllAncestors(LWLink link, LWComponent component) { LWContainer parent1 = null; LWContainer parent2 = null; if (link.getHead() != null) parent1 = link.getHead().getParent(); if (link.getTail() != null) parent2 = link.getTail().getParent(); // don't need to do anything if link doesn't cross a (logical) parent boundry if (parent1 == parent2) return; // also don't need to do anything if link is BETWEEN a parent and a child // (in which case, at the moment, we don't even see the link) if (link.isParentChildLink()) return; /* System.err.println("*** ENSURING " + l); System.err.println(" (parent) " + l.getParent()); System.err.println(" ep1 parent " + l.getHead().getParent()); System.err.println(" ep2 parent " + l.getTail().getParent()); */ LWContainer commonParent = link.getParent(); if (commonParent == null) { System.out.println("ELPOTOAP: ignoring link with no parent: " + link + " for " + component); return; } if (commonParent != component.getParent()) { // If we don't have the same parent, we may need to shuffle the deck // so that any links to us will be sure to paint on top of the parent // we do have, so you can see the link goes to us (this), and not our // parent. todo: nothing in runtime that later prevents user from // sending link to back and creating a very confusing visual situation, // unless all of our parents happen to be transparent. LWComponent topMostParentThatIsSiblingOfLink = component.getParentWithParent(commonParent); if (topMostParentThatIsSiblingOfLink == null) { // this could happen for stuff in cutbuffer w/out parent? String msg = "FYI; ELPOTOAP couldn't find common parent for " + component; if (DEBUG.LINK) tufts.Util.printStackTrace(msg); else Log.info(msg); } else commonParent.ensurePaintSequence(topMostParentThatIsSiblingOfLink, link); } } /* public java.util.List getAllConnectedNodes() { // todo opt: could cache this list, or organize as giant iterator tree // see LWComponent comment for where to go next // Could also make this faster with a cached bounds // and explicitly call a getRepaintRegion here that // just returns that + any nodes(links) connected to any children java.util.List list = super.getAllConnectedNodes(); list.addAll(children); java.util.Iterator i = children.iterator(); while (i.hasNext()) { LWComponent c = (LWComponent) i.next(); list.addAll(c.getAllConnectedNodes()); } return list; } public java.util.List getAllConnectedComponents() { java.util.List list = super.getAllConnectedComponents(); list.addAll(children); java.util.Iterator i = children.iterator(); while (i.hasNext()) { LWComponent c = (LWComponent) i.next(); list.addAll(c.getAllConnectedComponents()); } return list; } */ /** * The default is to get all ChildKind.PROPER children */ @Override public Collection<LWComponent> getAllDescendents() { return getAllDescendents(ChildKind.PROPER); } /** * The default Order is Order.TREE, and bag is an ArrayList */ @Override public Collection<LWComponent> getAllDescendents(final ChildKind kind) { return getAllDescendents(kind, new java.util.ArrayList(numChildren()*2), Order.TREE); } @Override public Collection<LWComponent> getAllDescendents(final ChildKind kind, final Collection bag, Order order) { if (DEBUG.PARENTING) Log.debug("getAllDescendents " + kind + "," + order + "; in=" + Util.tags(bag)); final boolean visibleOnly = (kind == ChildKind.VISIBLE || kind == ChildKind.EDITABLE); final boolean editableOnly = (kind == ChildKind.EDITABLE); for (LWComponent child : getChildren()) { if (visibleOnly) { // This is the central place where VISIBLE/EDITABLE logic is handled, // tho there is some in LWMap in dealing with layers as well. Note: be // careful with changes here: breaking this, or the semantics of // isHidden / isFiltered, will break all sorts of operations throughout // VUE that fetch descendent lists. if (child.isHidden()) { // stop all descent: if a node isn't visible, it's children // are guaranteed not to be visible (and thus also not editable). continue; } else if (child.isFiltered()) { // we still want to process any further children who may not be filtered, // and thus may still be visible, but we can ignore this child // as being filtered means it's not visible or editable. // NOTE: kind of extreme for filtering have an effect this deep in the model -- // would be better as part of some kind of base traversal code (for renderers, pickers, searchers) child.getAllDescendents(kind, bag, order); continue; } if (editableOnly && (child.isLocked() || child.hasFlag(Flag.ICON))) { // ICON is set for node-icons // no descent: children of locked items are also locked continue; } // go ahead and process normally, paying attention or Order (TREE or DEPTH), // as we'll now be adding both this child and it's descendents. } if (order == Order.TREE) { bag.add(child); // outline-style: parent added before children child.getAllDescendents(kind, bag, order); } else { // Order.DEPTH: child.getAllDescendents(kind, bag, order); bag.add(child); // depth style: deepest components appear first (children before parents) } } // super.getAllDescendents(kind, bag, order); // is a NO-OP return bag; } /** @return an iterable containing all ChildKind.PROPER descendents that are instances of the given class */ public <A extends LWComponent> Iterable<A> getDescendentsOfType(ChildKind kind, Class<A> clazz) { return Util.typeFilter(getAllDescendents(kind), clazz); // final List list = new ArrayList(); // for (LWComponent c : getAllDescendents()) // if (clazz.isInstance(c)) // list.add(c); // return list; } /** @return an iterable containing all direct children that are instances of the given class */ public <A extends LWComponent> Iterable<A> getChildrenOfType(Class<A> clazz) { return Util.typeFilter(getChildren(), clazz); // final List list = new ArrayList(numChildren()); // for (LWComponent c : getChildren()) // if (clazz.isInstance(c)) // list.add(c); // return list; } /** same us using: for (LWNode node : getDescendentsOfType(LWNode.class)) { ... } */ @Override public Iterator<LWNode> getAllNodesIterator() { return getDescendentsOfType(LWNode.class).iterator(); } /** same as using: for (LWLink link : getDescendentsOfType(LWLink.class)) { ... } */ @Override public Iterator<LWLink> getAllLinksIterator() { return getDescendentsOfType(LWLink.class).iterator(); } // /** same us using: for (LWNode node : getChildrenOfType(LWNode.class)) { ... } */ // @Override // public Iterator<LWNode> getChildNodeIterator() { return getChildrenOfType(LWNode.class).iterator(); } // /** same us using: for (LWLink link : getChildrenOfType(LWLink.class)) { ... } */ // @Override // public Iterator<LWLink> getChildLinkIterator() { return getChildrenOfType(LWLink.class).iterator(); } // /** @deprecated - use getChildNodeIterator */ // public Iterator getNodeIterator() { return getChildNodeIterator(); } // /** @deprecated - use getChildLinkIterator */ // public Iterator getLinkIterator() { return getChildLinkIterator(); } /** @return the total number of descendents */ @Override public int getDescendentCount() { int count = 0; for (LWComponent c : getChildren()) { count++; count += c.getDescendentCount(); } return count; } // /** // * @deprecated -- use getAllDescendents variants // * Lighter weight than getAllDescendents, but must be sure not to modify // * map hierarchy (do any reparentings) while iterating or may get concurrent // * modification exceptions. // */ // public Iterator getAllDescendentsIterator() // { // VueUtil.GroupIterator gi = new VueUtil.GroupIterator(); // gi.add(getChildIterator()); // for (LWComponent c : getChildren()) { // if (c.hasChildren()) // gi.add(((LWContainer)c).getAllDescendentsIterator()); // } // return gi; // } @Override protected LWComponent defaultPickImpl(PickContext pc) { //return isDrawn() ? this : null; // should already be handled now in the PointPick traversal return this; } @Override protected LWComponent defaultDropTarget(PickContext pc) { return this; } /** subclasses can override this to change the picking results of children */ protected LWComponent pickChild(PickContext pc, LWComponent c) { return c; } public boolean isOnTop(LWComponent c) { return indexOf(c) == mChildren.size() - 1; } public boolean isOnBottom(LWComponent c) { return indexOf(c) == 0; } // public int getLayer(LWComponent c) // { // return indexOf(c); // } protected int indexOf(Object c) { if (isDeleted()) throw new IllegalStateException("*** Attempting to get index of a child of a deleted component!" + "\n\tdeleted parent=" + this + "\n\tseeking index of child=" + c); return mChildren == NO_CHILDREN ? -1 : mChildren.indexOf(c); } /* To preseve the relative display order of a group of elements * we're moving forward or sending back, we need to move them in a * particular order depending on the operation and how the * operation functions. Note that when moving a group * forward/back one move at a time, once the group moves all the * way to the front or the back of the list, it will start cycling * the order of the components if they're all right next to each * other. */ protected static final Comparator ForwardOrder = new Comparator<LWComponent>() { public int compare(LWComponent c1, LWComponent c2) { return c2.getParent().indexOf(c2) - c1.getParent().indexOf(c1); }}; protected static final Comparator ReverseOrder = new Comparator<LWComponent>() { public int compare(LWComponent c1, LWComponent c2) { LWContainer parent1 = c1.getParent(); LWContainer parent2 = c2.getParent(); if (parent1 == null) return Short.MIN_VALUE; else if (parent2 == null) return Short.MAX_VALUE; if (parent1 == parent2) return parent1.indexOf(c1) - parent1.indexOf(c2); else return parent1.getDepth() - parent2.getDepth(); }}; /** If all the children do not have the same parent, the sort order won't be 100% correct. */ public static final Comparator ZOrderSorter = new Comparator<LWComponent>() { public int compare(LWComponent c1, LWComponent c2) { final LWContainer parent1 = c1.getParent(); final LWContainer parent2 = c2.getParent(); // We can't get z-order on a node if it's an orphan (no // parent), which is what any paste's or system drags // will get us. So we'll need to keep a sync'd a z-order // value in LWComponent to support this in all cases. if (parent1 == parent2) return parent1.getChildren().indexOf(c1) - parent2.getChildren().indexOf(c2); else return 0; // it's possible to figure out which parent is deepest, // but we'll save that for later. } }; protected static LWComponent[] sort(Collection<? extends LWComponent> bag, Comparator comparator) { LWComponent[] array = new LWComponent[bag.size()]; bag.toArray(array); java.util.Arrays.sort(array, comparator); // Note that it's okay that components with different // parents are in this list, as they only need to move // relative to layers of any other siblings in the list, // and it doesn't matter if re-layering is done to // a parent (LWContainer) at a time -- only that the movement // order of siblings within a parent is enforced. return array; } /** * Make component(s) paint first & hit last (on bottom) */ public static void bringToFront(List selectionList) { LWComponent[] comps = sort(selectionList, ReverseOrder); for (int i = 0; i < comps.length; i++) comps[i].getParent().bringToFront(comps[i]); } public static void bringForward(List selectionList) { LWComponent[] comps = sort(selectionList, ForwardOrder); for (int i = 0; i < comps.length; i++) comps[i].getParent().bringForward(comps[i]); } /** * Make component(s) paint last & hit first (on top) */ public static void sendToBack(List selectionList) { LWComponent[] comps = sort(selectionList, ForwardOrder); for (int i = 0; i < comps.length; i++) comps[i].getParent().sendToBack(comps[i]); } public static void sendBackward(List selectionList) { LWComponent[] comps = sort(selectionList, ReverseOrder); for (int i = 0; i < comps.length; i++) comps[i].getParent().sendBackward(comps[i]); } public boolean bringToFront(LWComponent c) { if (mChildren == NO_CHILDREN) { Util.printStackTrace("no children " + this + "; " + c); return false; } // Move to END of list, so it will paint last (visually on top) int idx = mChildren.indexOf(c); int idxLast = mChildren.size() - 1; if (idx < 0 || idx == idxLast) return false; //System.out.println("bringToFront " + c); notify(LWKey.HierarchyChanging); mChildren.remove(idx); mChildren.add(c); // we layout the parent because a parent node may lay out // it's children in the order they appear in this list notify("hier.move.front", c); c.getParent().layoutChildren(); return true; } public boolean sendToBack(LWComponent c) { if (mChildren == NO_CHILDREN) { Util.printStackTrace("no children " + this + "; " + c); return false; } // Move to FRONT of list, so it will paint first (visually on bottom) int idx = mChildren.indexOf(c); if (idx <= 0) return false; //System.out.println("sendToBack " + c); notify(LWKey.HierarchyChanging); mChildren.remove(idx); mChildren.add(0, c); notify("hier.move.back", c); c.getParent().layoutChildren(); return true; } public boolean bringForward(LWComponent c) { if (mChildren == NO_CHILDREN) { Util.printStackTrace("no children " + this + "; " + c); return false; } // Move toward the END of list, so it will paint later (visually on top) int idx = mChildren.indexOf(c); int idxLast = mChildren.size() - 1; if (idx < 0 || idx == idxLast) return false; //System.out.println("bringForward " + c); notify(LWKey.HierarchyChanging); swap(idx, idx + 1); notify("hier.move.forward", c); c.getParent().layoutChildren(); return true; } public boolean sendBackward(LWComponent c) { if (mChildren == NO_CHILDREN) { Util.printStackTrace("no children " + this + "; " + c); return false; } // Move toward the FRONT of list, so it will paint sooner (visually on bottom) int idx = mChildren.indexOf(c); if (idx <= 0) return false; //System.out.println("sendBackward " + c); notify(LWKey.HierarchyChanging); swap(idx, idx - 1); notify("hier.move.backward", c); c.getParent().layoutChildren(); return true; } private void swap(int i, int j) { //System.out.println("swapping positions " + i + " and " + j); mChildren.set(i, mChildren.set(j, mChildren.get(i))); } // essentially this implements an "insert-after" of top relative to bottom void ensurePaintSequence(LWComponent onBottom, LWComponent onTop) { if (mChildren == NO_CHILDREN) { Util.printStackTrace("no children " + this + "; bot=" + onBottom + "; top=" + onTop); return; } if (onBottom.getParent() != this || onTop.getParent() != this) { System.out.println(this + "ensurePaintSequence: both aren't children " + onBottom + " " + onTop); return; //throw new IllegalArgumentException(this + "ensurePaintSequence: both aren't children " + onBottom + " " + onTop); } int bottomIndex = indexOf(onBottom); int topIndex = indexOf(onTop); if (bottomIndex < 0 || topIndex < 0) { if (DEBUG.Enabled) Util.printStackTrace(this + "ensurePaintSequence: both aren't in list! " + bottomIndex + " " + topIndex); return; } //if (DEBUG.PARENTING) System.out.println("ENSUREPAINTSEQUENCE: " + onBottom + " " + onTop); if (topIndex == (bottomIndex - 1)) { if (DEBUG.PARENTING) out("ensurePaintSequence: swapping adjacents " + onTop); notify(LWKey.HierarchyChanging); swap(topIndex, bottomIndex); notify("hier.sequence"); } else if (topIndex < bottomIndex) { if (DEBUG.PARENTING) out("ensurePaintSequence: re-inserting after botIndex " + onTop); notify(LWKey.HierarchyChanging); mChildren.remove(topIndex); // don't forget that after above remove the indexes have all been shifted down one if (bottomIndex >= mChildren.size()) mChildren.add(onTop); else mChildren.add(bottomIndex, onTop); notify("hier.sequence"); } else { if (DEBUG.PARENTING) out("ensurePaintSequence: already sequenced: " + onTop); } //if (DEBUG.PARENTING) System.out.println("ensurepaintsequence: " + onBottom + " " + onTop); } @Override protected void setScale(double scale) { //System.out.println("Scale set to " + scale + " in " + this); super.setScale(scale); layoutChildren(); // we do this for our rollover zoom hack so children are repositioned } /** * Default impl just fills the background and draws any children. */ @Override protected void drawImpl(DrawContext dc) { //if (!isTransparent()) { final Color fill = getRenderFillColor(dc); if (fill != null) { dc.g.setColor(fill); dc.g.fill(getZeroShape()); } if (getStrokeWidth() > 0) { dc.g.setStroke(this.stroke); dc.g.setColor(getStrokeColor()); dc.g.draw(getZeroShape()); } drawChildren(dc); } protected void drawChildren(DrawContext dc) { if (hasChildren() == false) return; for (LWComponent c : getChildren()) { //------------------------------------------------------- // Using a requiresPaint is a huge speed optimzation. // Eliminating all the Graphics2D calls that would end up // having to check the clipBounds internally makes a big // difference. // ------------------------------------------------------- if (c.requiresPaint(dc)) { drawChildSafely(dc, c); } } //if (DEBUG) out("PAINTED " + types); } private void drawChildSafely(DrawContext _dc, LWComponent c) { // todo opt: potentially use dc.push/pop that instead of creating & disposing // GC's, records/resets the transform. final DrawContext dc = _dc.create(); try { drawChild(c, dc); } catch (Throwable t) { synchronized (System.err) { tufts.Util.printStackTrace(t); System.err.println("*** Exception drawing: " + c); System.err.println("*** In parent: " + this); System.err.println("*** DC parent: " + _dc); System.err.println("*** DC child: " + dc); System.err.println("*** Graphics parent: " + _dc.g); System.err.println("*** Graphics child: " + dc.g); System.err.println("*** Transform-start: " + _dc.g.getTransform()); System.err.println("*** Transform-end: " + dc.g.getTransform()); System.err.println("*** clip: " + dc.g.getClip()); System.err.println("*** clipBounds: " + dc.g.getClipBounds()); } } finally { dc.dispose(); } } protected void drawChild(LWComponent child, DrawContext dc) { child.drawLocal(dc); } /** * Be sure to duplicate all children and set parent/child references, * and if we weren't given a LinkPatcher, to patch up any links * among our children. */ @Override public LWContainer duplicate(CopyContext cc) { boolean isPatcherOwner = false; if (cc.patcher == null && cc.dupeChildren && hasChildren()) { // Normally VUE Actions (e.g. Duplicate, Copy, Paste) // provide a patcher for duplicating a selection of // objects, but anyone else may not have provided one. // This will take care of arbitrary single instances of // duplication, including duplicating an entire Map. cc.patcher = new LinkPatcher(); isPatcherOwner = true; } final LWContainer containerCopy = (LWContainer) super.duplicate(cc); if (cc.dupeChildren && mChildren != NO_CHILDREN) { containerCopy.mChildren = new ArrayList(mChildren.size()); for (LWComponent c : getChildren()) { LWComponent childCopy = c.duplicate(cc); containerCopy.mChildren.add(childCopy); childCopy.setParent(containerCopy); } } if (isPatcherOwner) cc.patcher.reconnectLinks(); return containerCopy; } @Override public String paramString() { if (hasChildren()) return super.paramString() + " chld=" + numChildren(); else return super.paramString(); } }