/* * 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.*; import java.util.*; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.Color; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.AffineTransform; /** * * Manage a group of LWComponents. A group defines it's bounds by it's members. * Default top-level behavour for "groups" as generally defined is that selecting * anything in a group with a default selection tool will actually select the group * itself, and allow one to reposition the group with all it's memebers at once, * such that all members maintain their relative position to one another. * * By default, groups in VUE have no border or fill, but these can be set * if desired. Also significantly, the entire group can be dropped into * a scaled context (or scaled itself), and all members will still keep * stable positions relative to each other in the scaled context. * * @author Scott Fraize * @version $Revision: 1.96 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $ */ // TODO: the FORMING of groups is broken on slides -- the new children are repositioned! public class LWGroup extends LWContainer { private static final boolean FancyGroups = true; private boolean isForSelection = false; public LWGroup() { if (!FancyGroups) disablePropertyTypes(KeyType.STYLE); disableProperty(LWKey.Font); disableProperty(LWKey.FontName); disableProperty(LWKey.FontSize); disableProperty(LWKey.FontStyle); disableProperty(LWKey.TextColor); } @Override public boolean supportsChildren() { return FancyGroups; } @Override public boolean supportsUserResize() { return false; } // if (FancyGroups) // return !isTransparent(); // else // return false; // } @Override public boolean supportsUserLabel() { return false; } @Override public boolean isOrphan() { return isForSelection || super.isOrphan(); } /** * For the viewer selection code -- we're mainly interested * in the ability of a group to move all of it's children * with it. */ void useSelection(LWSelection selection) { if (!isForSelection) Util.printStackTrace("NOT FOR SELECTION!: " + this); Rectangle2D bounds = selection.getBounds(); if (bounds != null) { super.setSize((float)bounds.getWidth(), (float)bounds.getHeight()); super.setLocation((float)bounds.getX(), (float)bounds.getY()); } else { System.err.println("null bounds in LWGroup.useSelection"); } super.mChildren = selection; } /** * Create a new LWGroup, reparenting all the LWComponents * in the selection to the new group. */ static LWGroup create(LWSelection selection) { final LWGroup group = new LWGroup(); // performance: pre-allocate the child list for the maximum child content size group.mChildren = new ArrayList(selection.size()); group.createFromNonLinks(selection); if (DEBUG.CONTAINMENT) System.out.println("CREATED: " + group); return group; } /** * Establish a newly created group from the memebrs of the given selection. * Will initially ignore any links (for a variety of reasons) and leave * it up each link cleanup task to decide if it should add itself to the group. * This also makes sure to maintain the relative z-order of the imported nodes. */ private void createFromNonLinks(Collection<LWComponent> selection) { // The creation of new groups, especially ones containing links, and especially // curved links, creates a number of thorny problems that need to be dealt with // to do it right, and to make sure undo of the group creation is going to work. // When the new children of the group are pulled from their existing parent, we // want to localize the coordinates within the new group (this happens // automatically). However, if the new group doesn't know it's bounds yet, we // can't meaninfully localize the coordinates. So when creating groups, we use // setShapeFromContents to provide the initial location and size of the group // (as opposed to relying on generic normalization). We can't normalize until // the group has at least bootstrapped to know it's initial location (the upper // left hand corner of the upper left most member). // There are also problems with adding curved links initially at group creation // time (UNDO breaks: see below), so we never allow the explicit grabbing of a // link into a group when it is initially created. The links will add // themselves as needed later via the link cleanup task. So newly created // groups go through two phases: in the first phase, completed in this method, // there will be a group containing all non-links, fully normalized when we're // done. The normalization process is different during creation in that we set // the location & size first (based on just the non-link content), then we // normalize only the added children (by calling normalize(false)) -- so their // locations are properly relative to the newly created group. In the second // phase, all connected links to anything that was added to the group will // automatically run their cleanup tasks (having detected changes in the // endpoints), and will then be able to join an already stabilized group and // map-parented group, so that if it's a curved link, when localizeCoordinates // is called to translate it's control points into the new group coordinate // space, the undo manager will be able to record the old positions of those // control points, so they can be faithfully restored on undo. Theoretically, // localizeCoordinates / setLocation on the entire link could handle this during // undo, tho we've had so many problems faithfully handling the redirection of // setLocation on links to a translate for all link sub-points (as links don't // have a 0,0 of their own: just sub-points recorded within their parent), that // ensuring we have real coordinates to restore for undo is the only safe way to // handle this. // Another potential problem is that if we're creating this new group inside // another group, there's an issue with the cleanup tasks: there's nothing that // ensures that the child group's cleanup task runs before the parents, which // would be critical if a newly created group were to rely on it's standard // later normalization pass to sort itself out. This is why on creation, we // ensure the group is in a fully normalized and stable state before it's done // being created, even if later it will be re-normalized by link cleanup tasks // adding themselves to the group. We could be hit again someday with this // issue of cleanup tasks not enforcing a depth-first task order, tho our // current FIFO ordering appears to cover us (as long as the group is normalized // by the end of it's creation). final Collection<LWComponent> reparenting; if (true) { reparenting = new HashSet<LWComponent>() { @Override public boolean add(LWComponent c) { //if (c instanceof LWLink && ((LWLink)c).isBound()) //// don't add any links that are connected to anything: they'll reparent themselves if (c instanceof LWLink) { // don't add any links AT ALL for now -- safer -- see below problem comment return false; } else return super.add(c); } }; reparenting.addAll(selection); setShapeFromContents(reparenting); } else { //---------------------------------------------------------------------------------------- // If both ends of any link are in the selection of what's being added to the // group, or are descendents of what's be added to the group, and that link's // parent is not already something other than the default link parent, scoop it // up as a proper child of the new group. // // Although the link cleanup task also enforces this condition, adding this code // here is much more perfomant when creating large groups, and it simplifies the // initial group creation code. // // OOPS -- PROBLEM: if we grab ANY links here, if they have any control points, when // their coordinates are localized, there's no undo manager to catch those events, // because this is a group under creation, and as such isn't in the model till // it's done being created! (Mind you, this is also true for nodes, but curved // links have this special case problem of trying to undo a translate, as links // don't have a real location...) // // So the upshot is that our slow method of only adding links at the end // with cleanup tasks handles this better, because then the undo manager // gets all the events needed to sort things out... //---------------------------------------------------------------------------------------- //final Collection<LWComponent> reparenting = new HashSet(); reparenting = new HashSet(); final Collection<LWComponent> allUniqueDescendents = new HashSet(); for (LWComponent c : selection) { reparenting.add(c); allUniqueDescendents.add(c); c.getAllDescendents(ChildKind.PROPER, allUniqueDescendents); } final HashSet uniqueLinks = new HashSet(); for (LWComponent c : allUniqueDescendents) { if (DEBUG.PARENTING) out("ALL UNIQUE " + c); for (LWLink l : c.getLinks()) { boolean bothEndsInPlay = !uniqueLinks.add(l); if (DEBUG.PARENTING) out("SEEING LINK " + l + " IN-PLAY=" + bothEndsInPlay); //if (bothEndsInPlay && l.getParent() instanceof LWMap) { // why this LWMap check? is old in any case: need to allow slides // TODO: right way to do this: only if link parent is currently the same as the top // level parent all the selection contents are coming from... if (bothEndsInPlay && !(c.getParent() instanceof LWGroup)) { // don't pull out of embedded group if (DEBUG.PARENTING) out("GRABBING " + l + " (both ends in group)"); reparenting.add(l); } } } setShapeFromContents(reparenting); } //---------------------------------------------------------------------------------------- // Be sure to preserve the current relative ordering of all // these components the new group. addChildren(sort(reparenting, LWContainer.ReverseOrder)); // At first we set our size and location for all the imported nodes (no links) // above via setShapeFromContents. Now that we've imported the non-link // children, they need to be normalized (coordinates made relative to the group). // In this special init bootstrapping case, we tell normalize NOT to change // the location or size of the group itself -- we already know that, and // to do that again before the new children's coordinates are made relative // would produce bogus results for the new location. normalize(false); } /** * "Borrow" the children in the list for the sole purpose of computing total bounds * and moving them around en-mass -- used for dragging a selection. Does NOT * reparent the components in any way. */ // TODO: get rid of this and just have useSelection, or move this code to // LWSelection itself. (Maybe have LWSelection subclass LWContainer so it can draw, // etc) -- this selection special stuff is a mess to have in LWGroup, which is // already some rediculously complicated code. static public LWGroup createTemporary(java.util.ArrayList selection) { LWGroup group = new LWGroup(); group.isForSelection = true; if (DEBUG.Enabled) group.setLabel("<=SELECTION=>"); group.mChildren = (java.util.ArrayList) selection.clone(); group.setShapeFromChildren(); if (DEBUG.CONTAINMENT) System.out.println("LWGroup.createTemporary " + group); return group; } private void setShapeFromContents(Iterable<LWComponent> contents) { // As we only allow creating groups from elements that all have the same parent, // we can use getLocalBorderBounds, which we also need to do to make sure // the location of the newly created group is correct within it's new parent. final Rectangle2D.Float bounds = LWMap.getLocalBorderBounds(contents); super.setSize(bounds.width, bounds.height); super.setLocation(bounds.x, bounds.y); } private void setShapeFromChildren() { setShapeFromContents(getChildren()); } protected void normalize() { normalize(true); } /** * Normalize the group. * * The process of normalization is to expand/contract the group to fit the new * bounds of our contents if they've moved/resized, and then update the local * coordinates of our members to reflect their offset with the new group bounds, if * the upper left hand corner of the group has changed (it's 0,0 position in it's * parent has changed). The point is to update the local child locations relative * to any new group position such that there is no net change to their absolute * position on the map. So: if a group member has simply moved within the group * without moving beyond any current edge of the group, there is nothing to do. If * a group member has moved below or to the right if our current bounds, the group * simply needs to increase it's size. Howver, if a member has moved above or to * the left of our current bounds, the group needs to change it's location (as well * as size), then "normalize" all the children: translate them down and to the right * by the exact amount the group has moved up and to the left. * * @param reshape - if true, allow reshaping of the group. This is the standard * case, except during group creation, where in order to bootstrap ourseleves, * the group separately estalishes an initial bounds, and then updates the child * locations. * */ private void normalize(boolean reshape) { if (DEBUG.WORK) {System.out.println(); out("NORMALIZING" + (reshape?"":" W/OUT RESHAPE FOR INIT"));} final Rectangle2D.Float curBounds = getLocalBounds(); final Rectangle2D.Float preBounds = getPreNormalBounds(); final float dx = preBounds.x; final float dy = preBounds.y; if (DEBUG.WORK) { final float dw = preBounds.width - curBounds.width; final float dh = preBounds.height - curBounds.height; out("curBounds: " + Util.out(curBounds)); out("preBounds: " + Util.out(preBounds) + String.format("; dx=%+.2f dy=%+.2f dw=%+.1f dh=%+.1f", dx, dy, dw, dh)); //out("preBounds: " + Util.out(preBounds) + "; dx=" + dx + " dy=" + dy); } if (dx != 0.0f || dy != 0.0f) { // we must call the event generating setter for undo to work (v.s. takeLocation) // However, when the group normalizes it's location, we do not want to issue // mapLocationChanged calls to all descendents, as normalization is all // about changing the location of the group without changing the on-map // location of any of it's members, so we make the special call to // setLocation here that allows this. if (reshape) super.setLocation(getX() + dx, getY() + dy, this, false); // if (true) { // Actions.MakeCircle.act(new LWSelection(getChildren())); // return; // } // Could theoretically handle this via mapLocationChanged calls, if above // was a real call to setLocation, but then there'd have to be a check // to see if the parent specifically was an LWGroup. if (DEBUG.WORK) out("normalizing relative children: dx=" + dx + " dy=" + dy); for (LWComponent c : getChildren()) { // we don't really need an event here (any descendents have already // been called with mapLocationChhanged if they need it due to // the above call to setLocation), and this translation is indended // to leave the component at it's exact current map location -- it's // position is only changing relative to it's parent group, which // was reshaped to the northwest -- this call moves all our non // absolute (non-link) members the same amount to the southeast. // HOWEVER, we still need to generate an event for undo... // So we're calling translate (instead of takeTranslation) c.translate(-dx, -dy); if (c instanceof LWLink) { // do we still need this? // links to links can get out of sync with updates // depending on the order they exist in the child list. // this should help for at least most first tier cases: ((LWLink)c).layout(); } } } if (DEBUG.WORK) { out(String.format("curShape %f,%f %fx%f", getX(), getY(), super.width, super.height)); out(String.format("newShape %f,%f %fx%f", dx, dy, preBounds.width, preBounds.height)); } if (reshape) super.setSize(preBounds.width, preBounds.height); if (DEBUG.WORK) out("NORMALIZED"); } /** @return the bounds of of contents prior to normalization: these bounds * are likely to be different than our current bounds: the process of normalization * ensures that the group bounds eventually match these bounds. The bounds * are in the parent-local coordinate space (the groups). So, for instance, if * the upper left most member of a group moves up 10 pixels, the pre-normalized * upper left hand corner bounds of the contents will be at 0,-10, relative to * the current group (the parent), and the pre-normal height will be 10 pixels * greater than the current height of the group. */ private Rectangle2D.Float getPreNormalBounds() { if (numChildren() < 2) // if only zero or one child, we should be about to disperse... return LWMap.EmptyBounds; else return LWMap.getLocalBorderBounds(getChildren()); } @Override protected void removeChildrenFromModel() { // groups don't actually delete their children // when the group object goes away. Overriden // so LWContainer.removeChildrenFromModel // is skipped. } private class DisperseOrNormalize implements Runnable { final Object srcMsg; DisperseOrNormalize(Object srcMsg) { this.srcMsg = srcMsg; } public void run() { if (isDeleted()) return; if (numChildren() < 2) { if (DEBUG.PARENTING || DEBUG.CONTAINMENT) out("AUTO-DISPERSING on child count " + numChildren()); disperse(); } else { normalize(); } } public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()) + "[" + srcMsg + "]"; } } private void requestCleanup(Object srcMsg) { //super.addCleanupTask(new DisperseOrNormalize(srcMsg)); // always allocates the darn task.. //if (DEBUG.CONTAINMENT) out("requestCleanup on " + srcMsg); final UndoManager um = getUndoManager(); if (um != null && !um.isUndoing() && !um.hasLastTask(this)) { if (DEBUG.CONTAINMENT) out("addLastTask/DisperseOrNormalize; on " + srcMsg); //if (DEBUG.Enabled) out(TERM_RED + "ADDING CLEANUP TASK on: " + srcMsg + TERM_CLEAR); //um.addCleanupTask(this, new DisperseOrNormalize(srcMsg)); //super.addCleanupTask(new DisperseOrNormalize(srcMsg)); um.addLastTask(this, new DisperseOrNormalize(srcMsg)); } } @Override protected void removeChildren(Iterable<LWComponent> iterable, Object context) { super.removeChildren(iterable, context); requestCleanup("removeChildren"); } @Override public void addChildren(Collection<? extends LWComponent> iterable, Object context) { super.addChildren(iterable, context); requestCleanup("addChildren"); } /** * Remove all children and re-parent to this group's parent, * then remove this now empty group object from the parent. */ public void disperse() { // todo: better to insert all the children back into the // parent at the layer of group object instead of on top of // everything else. if (DEBUG.PARENTING || DEBUG.CONTAINMENT) System.out.println("DISPERSING: " + this); if (hasChildren()) { final LWContainer newParent = getParentOfType(LWContainer.class); final List tmpChildren = new ArrayList(mChildren); //if (DEBUG.PARENTING || DEBUG.CONTAINMENT) out("DISPERSING " + tmpChildren.size() + " children"); // we can brute-force remove our children, to skip de-parenting events, // as this group will be dissapeared at the end of this operation anyway // TODO: this is probably screwing up UNDO tho... //this.children.clear(); newParent.addChildren(tmpChildren); } getParent().deleteChildPermanently(this); } // /* groups are always transparent -- defer to parent for background fill color */ // @Override // public java.awt.Color getRenderFillColor(DrawContext dc) // { // if (FancyGroups) // return super.getRenderFillColor(dc); // if (dc != null && (dc.focal == this || getParent() == null)) // return dc.getFill(); // else if (getParent() != null) // return getParent().getRenderFillColor(dc); // else // return null; // } // public java.awt.Color getFillColor() // { // if (FancyGroups) // return super.getFillColor(); // else // return null; // } @Override public void setMapLocation(double x, double y) { if (isForSelection) { final double dx = x - getX(); final double dy = y - getY(); translateSelection(dx, dy); super.setLocation((float)x, (float)y); } else super.setMapLocation(x, y); } private void translateSelection(double dx, double dy) { for (LWComponent c : getChildren()) { // If parent and some child both in selection and you drag, the selection // (an LWGroup) and the parent fight to control the location of the child. // There may be a cleaner way to handle this, but checking it here works. // Also, do NOT skip if we're in a group -- that condition is caught below. // (could check for selected ancestor of type LWGroup.class) if (c.isSelected() && c.isAncestorSelected()) continue; else translateOnMap(c, dx, dy); } } /** translate across the map in absolute map coordinates -- special use by LWGroup */ private static void translateOnMap(LWComponent c, double dx, double dy) { // If this node exists in a scaled context, which means it's parent is scaled or // the parent itself is in a scaled context, we need to adjust the dx/dy for // that scale. The scale of this object being "dragged" by the call to // translateOnMap is irrelevant -- here we're concerned with it's location in // it's parent, not it's contents. So we need to beef up the translation amount // by the context scale so drags across the map will actually stay with the // mouse. E.g., if this object exists in a parent scaled down 50% (scale=0.5), // to move this object 2 pixels to the right in absolute top-level map // coordinates, we need to change it's internal location within it's parent by 4 // pixels (2 / 0.5 = 4) to have that show up on the map (when itself displayed // at 100% scale) as a movement of 4 pixels. final double scale = c.getParent().getMapScale(); if (scale != 1.0) { dx /= scale; dy /= scale; } c.translate((float) dx, (float) dy); // This will dramatically speed up drags of large groups of nodes (event creation // and delivery is skipping, the UndoManager doesn't need to sort them all out, etc) // However the LAST time we do this (mouse-up), we need to generate the events for undo //c.takeTranslation((float) dx, (float) dy); // performance test for dragging large groups of nodes } @Override public void setLocation(float x, float y) { if (isForSelection) Util.printStackTrace("setLocation on selection group " + x + "," + y + " " + this); else super.setLocation(x, y); } private boolean linksAreTranslatingWithUs = false; @Override protected void notifyMapLocationChanged(LWComponent src, double mdx, double mdy) { if (!isForSelection) { try { // this is just an optimization -- it's okay to over-normalize, but it // makes sorting through the resulting diagnostic event stream easier. linksAreTranslatingWithUs = true; super.notifyMapLocationChanged(src, mdx, mdy); } finally { linksAreTranslatingWithUs = false; } } } @Override void broadcastChildEvent(LWCEvent e) { if (mXMLRestoreUnderway) { // okay to skip the actual event broadcast: nobody should be listening to us return; } // Until we can know if it's a bounds event (or have a layout event), // just make the bounds dirty no matter what, and just in case update // any connected links updateConnectedLinks(null); //if (DEBUG.EVENTS && DEBUG.CONTAINMENT) System.out.println(e + "; broadcastChildEvent inside " + this); super.broadcastChildEvent(e); // we don't actually want to req-request this during normalization if the // children are deliverying real child location events (unless using // takeLocation), but normalization runs during cleanup in the undo manager, and // until it's complete, the existing normalization task is still in the // UndoManager's cleanup queue, so we won't add another one. if (e.hasOldValue() && !linksAreTranslatingWithUs) { // only request cleanup if this event is real enough to be undoable // we're not interested in info-only events (really, we're only // interested in events that are "isBoundsEvents", but we // don't support that in Key's yet -- e.g., no point in responding // to color value changes -- no change of that changing our bounds. requestCleanup(e); } } @Override public void notifyHierarchyChanged() { requestCleanup("hierarchyChanged"); super.notifyHierarchyChanged(); } /** Overridden in to handle special selection LWGroup: if is asked to draw itself into another context (e.g., on an image), * it won't bother to transform locally -- just draw the children as they are. */ @Override public void draw(DrawContext dc) { if (isForSelection) drawChildren(dc); else super.draw(dc); } private final Color getPathwayColor() { final LWPathway exclusive = getExclusiveVisiblePathway(); if (exclusive != null) return exclusive.getColor(); else if (inPathway(VUE.getActivePathway()) && VUE.getActivePathway().isDrawn()) return VUE.getActivePathway().getColor(); else return null; } private static final boolean TrackPathwayColor = false; // We won't need this at all assumung we go back to handled only as pathway overlap @Override public Color getRenderFillColor(DrawContext dc) { if (TrackPathwayColor && dc != null && dc.drawPathways() && isTransparent()) { return getPathwayColor(); // final LWPathway exclusive = getExclusiveVisiblePathway(); // if (exclusive != null) // return exclusive.getColor(); // else if (inPathway(VUE.getActivePathway()) && VUE.getActivePathway().isDrawn()) // return VUE.getActivePathway().getColor(); } //return null; return super.getRenderFillColor(dc); } @Override protected void drawImpl(DrawContext dc) { // TODO: DON'T DRAW ANYTHING BUT CHILDREN IF WE'RE FILTERED OUT if (DEBUG.CONTAINMENT && !isForSelection) { java.awt.Shape shape = getZeroShape(); dc.g.setColor(new java.awt.Color(64,64,64,64)); dc.g.fill(shape); dc.g.setColor(java.awt.Color.blue); dc.setAbsoluteStroke(1.0); dc.g.draw(shape); } final Color fill; if (TrackPathwayColor) { if (dc.drawPathways() && dc.focal != this && isTransparent()) { final Color c = getPathwayColor(); if (c != null) fill = c; } else fill = getFillColor(); } else { // this REALLY should be an overlay (or just the old style border) not a back fill, to make clear this isn't // a fill color on the object. fill = getFillColor(); } if (!isFiltered()) { if (fill != null && fill.getAlpha() != 0) { 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); // if (FancyGroups) // // draw fill, border & children // super.drawImpl(dc); // else // // don't draw fill or border // drawChildren(dc); if (dc.isInteractive()) { if (isSelected() && dc.focal != this) { final Shape shape = getZeroShape(); if (DEBUG.CONTAINMENT) out("drawing selection bounds shape " + shape); dc.g.setColor(COLOR_HIGHLIGHT); dc.g.fill(shape); } else if (isZoomedFocus() && !hasDecoratedFeatures()) { dc.setAbsoluteStroke(1); dc.g.setColor(COLOR_SELECTION); dc.g.draw(getZeroShape()); } } } /** @return 1 */ @Override public int getPickLevel() { return 1; } @Override protected LWComponent pickChild(PickContext pc, LWComponent c) { // needed for groups embeeded in groups: if (pc.pickDepth > 0) return c; else if (c.isPathwayOwned() && c instanceof LWSlide) // to allow slide icon picking return c; else return this; } /** @return false unless decordated: groups contain no points themselves -- * only a point over a child is "contained" by the group. If decorated, * the standard impl applies of containing any point in the bounding box. */ @Override protected boolean containsImpl(final float x, final float y, PickContext pc) { if ((pc.isZoomRollover && pc.pickDepth < 1) || hasDecoratedFeatures() || pc.root == this) { // added check for us being the pick root (focal) so double-click would work to // get out of a zoomed focus return super.containsImpl(x, y, pc); } else return false; } @Override protected boolean intersectsImpl(final Rectangle2D rect) { if (isForSelection) return true; if (hasDecoratedFeatures()) { return super.intersectsImpl(rect); } else { for (LWComponent c : getChildren()) if (c.intersects(rect)) // todo: not in parent coords (?) return true; return false; } } private boolean hasDecoratedFeatures() { return getStrokeWidth() > 0 || !isTransparent(); // if (FancyGroups) // return getStrokeWidth() > 0 || !isTransparent(); // else // return getEntryToDisplay() != null; } } // TODO: ResizeControl still goes haywire if the group is in a scaled context, (is a // grand-child or deeper) tho at least it seems to stay local to the group and not start // translating all over the map -- we could maybe even live with this. // minor bug: if you group two nodes in list as child nodes, you get a tiny messy looking group. // should probably just not allow this. // minor bug (still?): when in a scaled context (grand-child or deeper), the bounds effect // of the stroke width of group members is being understated, so the group // bounds are a bit too small -- only show's up on selection tho (only // time we currently show group bounds), and it's a only off by a very small // amount -- not really noticable unless a very fat stroke and/or at huge zoom.