/*
* 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 tufts.vue.ds.Schema;
import tufts.vue.ds.Field;
import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.Color;
import java.awt.Font;
import java.awt.Stroke;
import java.awt.BasicStroke;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.AlphaComposite;
import java.awt.font.TextAttribute;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.awt.Transparency;
import java.util.*;
import java.util.regex.*;
import java.net.*;
import javax.swing.text.StyleConstants;
//import tufts.vue.beans.UserMapType; // remove: old SB stuff we never used
import tufts.vue.filter.*;
import edu.tufts.vue.metadata.MetadataList;
import edu.tufts.vue.metadata.VueMetadataElement;
/**
* VUE base class for all components to be rendered and edited in the MapViewer.
*
* This class is way too big. A bunch of inner classes could be separated out (e.g., Key, maybe
* Property, property event change raising (notify), etc). Beyond that, it could use a major separation of
* concerns (e.g., rendering, persistance, "dataSet" meta-data?).
*
* @version $Revision: 1.531 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $
* @author Scott Fraize
*/
// todo: on init, we need to force the constraint of size being set before
// label (applies to XML restore & duplicate) to support backward compat before
// line-wrapped text. Otherwise, in LWNode's, setting label before size set will cause
// the size to be set.
public class LWComponent
implements VueConstants, XMLUnmarshalListener
{
protected static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWComponent.class);
public enum ChildKind {
/** Include any and all children in the traversable LW hierarchy, such as slides and their
* children (pathway contents), and actual layer objects -- the only way to make sure you hit
* every active LWComponent in the runtime related to a particular LWMap (not including the
* Undo queue). This will return every LWComponent in the model that has an ID (getID).
* These are all components that are persisted in some way.
*/
ANY,
/** include only default, conceptually significant chilren, leaving out items such a slides, layers and pathways */
PROPER, // might better be termed what is a "user" or "user-content" object
/** VISIBLE is PROPER, excluding those that are not currently visible */
VISIBLE,
/** EDITABLE is VISIBLE, excluding those that are currently locked */
EDITABLE
// VIRTUAL -- would be *just* what ANY currently adds, and exclude PROPER -- currently unsupported
}
/** order of result set for getAllDescendents -- not applicable if collection passed in isn't ordered */
public enum Order {
/** default traversal order: parents before children */
TREE,
/** order for layout operations; children before parents */
DEPTH
};
/*
// need an IntegerPreference and/or an IntegerRangePreference (that ImagePreference could also use)
private static final VuePreference SlideIconPref =
IntegerPreference.create(edu.tufts.vue.preferences.PreferenceConstants.MAPDISPLAY_CATEGORY,
"slideIconSize",
"Slide Icon Size",
"Size of Slide icons displayed on the map",
true);
*/
private static final Object CAUSE_DEFAULT = "cause_default";
private static final Object CAUSE_PATHWAY = "cause_pathway";
//private static final Object CAUSE_PERSIST = "cause_persist";
public enum HideCause {
/** each subclass of LWComponent can use this for it's own purposes */
DEFAULT
/** we've been hidden by link pruning */
,PRUNE // (CAUSE_PERSIST),
/** another layer is set to be the exclusive layer */
,LAYER_EXCLUDED
/** we're a member of a pathway that hides when the pathway hides, and all pathways we're on are hidden */
,HIDES_WITH_PATHWAY (CAUSE_PATHWAY)
/** we've been hidden by a pathway that is in the process of revealing */
,PATH_UNREVEALED (CAUSE_PATHWAY)
/** we've been hidden because the current pathway is all we we want to see, and we're not on it */
,NOT_ON_CURRENT_PATH (CAUSE_PATHWAY)
/** we've been hidden due to the collapse of a parent (different from Flag.COLLAPSED, which is for the collapsed parent) */
,COLLAPSED
/** we're an LWImage that's a node-icon image, and we're hidden */
,IMAGE_ICON_OFF
// /** a search result has temporarily hidden us */
// ,SEARCH
;
final int bit = 1 << ordinal();
final Object type;
HideCause(Object typeKey) { type = typeKey; }
HideCause() { type = CAUSE_DEFAULT; }
}
private static Object FLAG_DEFAULT = "flag_default";
private static Object FLAG_UNDOABLE = "flag_undoable";
/** runtime flags explicitly set and cleared by VUE code -- not managed by UNDO */
public enum Flag {
/** been deleted (is in undo queue) */
DELETED,
/** we've been hidden due to filtering -- note that this isn't a hide-bit in that children may still be visible even when
* this is set*/
FILTERED,
/** is in the process of being deleted */
DELETING,
/** is in the process of being un-deleted (undo) */
UNDELETING,
/** is selected */
SELECTED,
/** is a component serving as a style source */
STYLE,
/** is a component serving as a data-style source */
DATA_STYLE,
/** cannot move, delete, link to or edit label */
LOCKED,
/** can't be moved */
FIXED_LOCATION,
/** has been specially styled for for appearance on a slide */
SLIDE_STYLE,
/** was created to serve some internal purpose: not intended to exist on a regular user map */
INTERNAL,
/** this component should NOT broadast change events */
EVENT_SILENT,
/** this component is in a "collapsed" or closed view state */
COLLAPSED
/** for links: this is data-relation link */
, DATA_LINK
/** for links: this is data-relation link and ALSO a data-count link */
, DATA_COUNT
/** currently used for marking LWImage's as being node-icons */
, ICON
/** for subclasses that want to distinguish between a default size and a validated size (e.g., LWImage)
* "default size" could actually mean any suggested or invalid size before a final definite size */
, UNSIZED
/** lets us know this is in the process of duplicating */
, DUPLICATING
/** this is part of a pruned map sub-graph */
, PRUNED
/** this is link with head pruned */
, PRUNE_HEAD
/** this is link with tail pruned */
, PRUNE_TAIL
;
// do we want a generalized LOCKED which means fixed,no-delete,no-duplicate?,no-reorder(forward/back),no-link?
public final int bit = 1 << ordinal();
final Object type;
Flag(Object typeKey) { type = typeKey; }
Flag() { this(FLAG_DEFAULT); }
}
/** runtime persistant flags, managed by UNDO */
public enum State
{
/** a map/layer has been auto-clustered by a data-drop */
HAS_AUTO_CLUSTERED
/** a map/layer has been auto-clustered by a data-drop */
, PRUNED
;
final int bit = 1 << ordinal();
final boolean persist;
State(boolean p) { persist = p; }
State() { persist = false; }
}
/** context codes for LWContainer.addChildren */
public static final Object ADD_DROP = "drop";
public static final Object ADD_PASTE = "paste";
public static final Object ADD_DEFAULT = "default";
public static final Object ADD_PRESORTED = "sorted";
public static final Object ADD_MERGE = "merge";
public static final Object ADD_CHILD_TO_SIBLING = "child-to-sibling";
//Static { for (Hide reason : Hide.values()) { System.out.println(reason + " bit=" + reason.bit); } }
public static final java.awt.datatransfer.DataFlavor DataFlavor =
tufts.vue.gui.GUI.makeDataFlavor(LWComponent.class);
public static final int MIN_SIZE = 10;
public static final Size MinSize = new Size(MIN_SIZE, MIN_SIZE);
public static final float NEEDS_DEFAULT = Float.MIN_VALUE;
public static final java.util.List<LWComponent> NO_CHILDREN = Collections.EMPTY_LIST;
public static final boolean COLLAPSE_IS_GLOBAL = true;
protected static boolean isGlobalCollapsed = false;
static void toggleGlobalCollapsed() {
if (!COLLAPSE_IS_GLOBAL)
throw new Error("disabled");
isGlobalCollapsed = !isGlobalCollapsed;
}
public interface Listener extends java.util.EventListener {
public void LWCChanged(LWCEvent e);
}
/*
* Meta-data persistant information
*/
protected String label = null; // protected for debugging purposes
private String notes = null;
private Resource resource = null;
private String mLabelFormat; // if there's a data-format, it's stored here
/*
* Persistent information
*/
private String ID = null;
// todo: next major re-architecting: instead of x/y width/height,
// keep a Point2D.Float bounds up to date (and can skip creating
// a rectangles constantly). (Might also keep a mapBounds?)
protected float x;
protected float y;
// TODO: if we want to support some kind of keep-relative alignment for an object
// (in it's parent), we couldn't just use a special object on a generic x/y value
// ptr -- we still need ACTUAL x/y values to render, but we could have an
// xAnchor/yAnchor, which could even be a list of actions to perform every time the
// object is laid out, or it's parent resizes.
//private MetadataList metadataList = new MetadataList();
private MetadataList metadataList = null;
private static final NodeFilter NEEDS_NODE_FILTER = new NodeFilter();
private NodeFilter nodeFilter = NEEDS_NODE_FILTER;
private URI uri;
protected float width = NEEDS_DEFAULT;
protected float height = NEEDS_DEFAULT;
/** creation time-stamp (when this node first joined a map) */
private long mCreated;
/** cached affine transform for use by getZeroTransform() */
private transient final AffineTransform _zeroTransform = new AffineTransform();
protected transient double scale = 1.0;
private transient AffineTransform mTemporaryTransform;
protected transient TextBox labelBox = null;
protected transient BasicStroke stroke = STROKE_ZERO;
//protected transient boolean selected = false;
protected int mHideBits = 0x0; // any bit set means we're hidden (not managed by undo)
//protected int mFilterBits = 0x0; // may need this to get pathway filtering not in conflict with search filtering
protected volatile int mFlags = 0x0; // explicitly set/cleared: not managed by undo
protected int mState = 0x0; // managed by undo (and individual bits may optionally be persisted)
protected transient LWContainer parent;
protected transient LWComponent mParentStyle;
protected transient LWComponent mSyncSource; // "semantic source" for nodes on slide to refer back to the concept map
protected transient Collection<LWComponent> mSyncClients; // set of sync sources that point back to us
/** list of links that contain us as an endpoint */
private transient List<LWLink> mLinks;
/** list of pathways that we are a member of */
private transient List<LWPathway> mPathways;
/** list of all pathway entries that refer to us (one for each time we appear on an individual pathway) */
protected transient List<LWPathway.Entry> mEntries;
/** properties for use by model clients (e.g., UI components) */
protected transient HashMap mClientData;
// need MetaMap (a multi-map) for XML data-sets that can have more than one value per key
protected transient MetaMap mDataMap;
// todo memory perf: mEntries should subclass ArrayList and implement this iter
// so they can be allocated together, instead of leaving this slot here unused
// for ever node w/out pathway entries.
private SlideIconIter mVisibleSlideIconIterator;
private transient long mSupportedPropertyKeys;
// TODO PERFORMANCE: change support could be handled generically, and we could at least lazy-create
protected transient final LWChangeSupport mChangeSupport = new LWChangeSupport(this);
protected transient boolean mXMLRestoreUnderway = false; // are we in the middle of a restore?
protected transient BufferedImage mImageBuffer;
public static final Comparator XSorter = new Comparator<LWComponent>() {
public int compare(LWComponent c1, LWComponent c2) {
// we multiply up the result so as not to loose differential precision in the integer result
return (int) (128f * (c1.x - c2.x));
}
};
public static final Comparator YSorter = new Comparator<LWComponent>() {
public int compare(LWComponent c1, LWComponent c2) {
return (int) (128f * (c1.y - c2.y));
}
};
public static final Comparator GridSorter = new Comparator<LWComponent>() {
public int compare(LWComponent c1, LWComponent c2) {
if (c1.y == c2.y)
return XSorter.compare(c1, c2);
else
return YSorter.compare(c1, c2);
}
};
/** constructor */
public LWComponent()
{
if (DEBUG.PARENTING) Log.debug("construct of " + Util.tag(this));
mSupportedPropertyKeys = Key.PropertyMaskForClass(getClass());
if (mSupportedPropertyKeys == 0) {
// this can happen during init before circular dependencies are resolved
if (DEBUG.INIT || DEBUG.STYLE) Util.printStackTrace("ZERO PROPERTY BITS IN " + Util.tag(this));
} else {
// not on by default:
disableProperty(KEY_Alignment);
}
}
// /** for internal proxy instances only */
// private LWComponent(String label) {
// setLabel(label);
// }
public long getSupportedPropertyBits() {
return mSupportedPropertyKeys;
}
/** Convenience: If key not a real Key (a String), always return true */
public boolean supportsProperty(Object key) {
if (key instanceof Key)
return supportsProperty((Key)key);
else
return false;
}
/** @return true if the given property is currently supported on this component */
public boolean supportsProperty(Key key) {
return (mSupportedPropertyKeys & key.bit) != 0;
}
public void disableProperty(Key key) {
disablePropertyBits(key.bit);
}
public void enableProperty(Key key) {
enablePropertyBits(key.bit);
}
protected void disablePropertyBits(long bits) {
mSupportedPropertyKeys &= ~bits;
}
protected void enablePropertyBits(long bits) {
mSupportedPropertyKeys |= bits;
}
protected void disablePropertyTypes(KeyType type) {
for (Key key : Key.AllKeys)
if (key.type == type || (type == KeyType.STYLE && key.type == KeyType.SUB_STYLE))
disableProperty(key);
}
/** Apply all style properties from styleSource to this component */
public void copyStyle(LWComponent styleSource) {
copyStyle(styleSource, ~0L);
}
public void copyStyle(LWComponent styleSource, long permittedPropertyBits) {
if (DEBUG.STYLE || styleSource == null) {
System.out.println("COPY STYLE of " + Util.tags(styleSource) + " ==>> " + Util.tags(this) + " permitBits=" + Long.bitCount(permittedPropertyBits));
}
if (styleSource == null)
return;
for (Key key : Key.AllKeys)
//if (key.isStyleProperty && styleSource.supportsProperty(key) && (permittedPropertyBits & key.bit) != 0)
if (styleSource.isStyling(key) && (permittedPropertyBits & key.bit) != 0)
key.copyValue(styleSource, this);
}
public void copyProperties(LWComponent source, long propertyBits) {
if (DEBUG.STYLE) System.out.println("COPY PROPS of " + source + " ==>> " + this + " bits=" + Long.bitCount(propertyBits));
for (Key key : Key.AllKeys)
if ((propertyBits & key.bit) != 0 && source.supportsProperty(key))
key.copyValue(source, this);
}
public void applyCSS(edu.tufts.vue.style.Style cssStyle)
{
System.out.println("Applying CSS style " + cssStyle.getName() + ":");
for (Map.Entry<String,String> se : cssStyle.getAttributes().entrySet()) {
final String cssName = se.getKey().trim().toLowerCase(); // todo: shouldn't have to trim this
final String cssValue = se.getValue().trim();
boolean applied = false;
System.err.format("%-35s CSS key %-17s value %-15s",
toString(),
'\'' + cssName + '\'',
'\"' + cssValue + '\"'
);
for (Key key : Key.AllKeys) {
if (key.cssName == null)
continue;
//out("Checking key [" + cssName + "] against [" + key.cssName + "]");
if(key.cssName.indexOf(";") > 0)
{
String[] names = key.cssName.split(";");
for(int i=0;i<names.length;i++)
{
if(supportsProperty(key) && names[i].equals(cssName))
{
applied = key.setValueFromCSS(this,names[i],cssValue);
}
}
}
else
if (supportsProperty(key) && cssName.equals(key.cssName)) {
//out("Matched supported property key " + key.cssName);
applied = key.setValueFromCSS(this, cssName, cssValue);
/*
final Property slot = key.getSlot(this);
if (slot == Key.NO_SLOT_PROVIDED) {
out("Can't apply CSS Style property to non-slotted key: " + cssName + " -> " + key);
} else {
try {
slot.setFromCSS(cssName, cssValue);
System.err.println("applied value: " + slot);
applied = true;
break;
} catch (Throwable t) {
System.err.println();
tufts.Util.printStackTrace(new Throwable(t), "failed to apply CSS key/value " + cssName + "=" + cssValue);
}
}
*/
}
}
setFont(cssStyle.getFont());
if (!applied)
System.err.println("UNHANDLED");
}
}
/**
* Describes a property on a VUE LWComponent, and provides an info string for creating Undo names,
* and for diagnostic output. Implies the ability to set/get the value on an LWComponent by some means.
*/
// todo: consdier moving all the Key/Property code to some kind of superclass to LWComponent -- LWStyle? Vnode? LWKey? LWState?
// We'd move it elsewhere, but we'd have to export all sorts of stuff to make all thats needed available,
// as they get everything currently being inner classes.
// The generic type TSubclass allows the inner-class impl's of getValue & setValue, in subclasses
// of LWComponent, to use their own type in the first argument to set/getValue, omitting
// the need for casts in the method.
public enum KeyType { Default, STYLE, SUB_STYLE, DATA };
// todo: TValue may be overkill -- may want to revert to using just Object
public static class Key<TSubclass extends LWComponent,TValue> {
/** A name for this key (used for undo labels & debugging) */
public final String name;
/** A name for a CSS property that can be used to initialize the value for this key */
public final String cssName;
/** The unique bit for this property key.
(Implies a max of 64 keys that can be known as active to our tools -- use a BitSet if need more) */
public final long bit;
// /** True if this key for a style property -- a property that moves from style holders to LWCopmonents
// * pointing to it via mParentStyle */
// public final boolean isStyleProperty;
public final KeyType type;
public final boolean isColor;
/* True this property is a sub-part of some other property */
//public final boolean isSubProperty;
public static final java.util.List<Key> AllKeys = new java.util.ArrayList<Key>();
private static int InstanceCount; // increment for each key instance, to establish the appropriate bit
private static final java.util.Map<Class,Long> ClassProperties = new java.util.HashMap<Class,Long>();
/** Get the supported property bit mask for the given class in the LWComponent inheritance tree
* This will only return accurate results after all Key's in the codebase have been initialized. */
static long PropertyMaskForClass(Class<? extends LWComponent> clazz) {
final Long bitsForClass = ClassProperties.get(clazz); // property bits for this class
if (bitsForClass == null) {
// If we found nothing, this must be the first instance of a new object
// for some subclass of LWComponent that doesn't declare any of it's
// own keys. Merge the bits for all superclasses and put it in the
// map for future reference.
long propMaskForClass = 0L;
for (Class c = clazz; c != null; c = c.getSuperclass())
propMaskForClass |= PartialPropertyMaskForClass(c);
if (DEBUG.INIT) Log.debug(String.format("CACHED PROPERTY BITS for %s: %d", clazz, Long.bitCount(propMaskForClass)));
ClassProperties.put(clazz, propMaskForClass);
return propMaskForClass;
} else
return bitsForClass;
}
/** @return the currently stored property mask for the given class: only used during initialization
* Will return 0L (no bit set) if the given class is not in the map (e.g., java.lang.Object)
* This is used to disambiguate between properties that apply only to a particular
* LWComponent subclass while we produce the ultimate merged results for all classes in
* the hierarchy.
*/
private static long PartialPropertyMaskForClass(Class clazz) {
final Long bitsForClass = ClassProperties.get(clazz); // property bits for this class
if (bitsForClass == null)
return 0L;
else
return bitsForClass;
}
public boolean isStyleProperty() {
return type == KeyType.STYLE;
}
public Key(String name) {
this(name, KeyType.Default);
}
public Key(String name, KeyType keyType) {
this(name, null, keyType);
}
public Key(String name, String cssName) {
this(name, cssName, KeyType.STYLE);
}
protected Key(String name, String cssName, KeyType keyType) {
this.name = name;
this.cssName = cssName;
this.type = keyType;
this.isColor = name.endsWith("color"); // todo: hack -- make more explicit
if (InstanceCount >= Long.SIZE) {
this.bit = 0;
tufts.Util.printStackTrace(Key.class + ": " + InstanceCount + "th key created -- need to re-implement (try BitSet)");
} else
this.bit = 1 << InstanceCount;
AllKeys.add(this);
// Note: this only works if the key is in fact declared in the enclosing class to
// which it applies. If we want to declare keys elsewhere, we'll need to add
// a Class argument to the constructor.
final Class clazz = getClass().getEnclosingClass(); // the class that own's the Key
long propMaskForClass = (PartialPropertyMaskForClass(clazz) | bit); // add the new bit
// Now be sure to mix in all properties found in all super-classes:
for (Class c = clazz; c != null; c = c.getSuperclass())
propMaskForClass |= PartialPropertyMaskForClass(c);
ClassProperties.put(clazz, propMaskForClass);
if (DEBUG.INIT || DEBUG.STYLE)
Log.debug(String.format("KEY %-20s %-11s %-22s bit#%2d; %25s now has %2d properties",
name,
//isStyleProperty ? "STYLE;" : "",
keyType,
cssName == null ? "" : cssName,
InstanceCount,
clazz.getName(),
Long.bitCount(propMaskForClass)
));
InstanceCount++;
//System.out.println("BITS FOR " + LWImage.class + " " + PropertyMaskForClass(LWImage.class));
// Could build list of all key (and thus slot) values here for each subclass,
// but where would we attach it? Would need to pass in the class variable
// in the constructor, and hash it to a list for the class. Then the
// problem would be that each list would only contain the subclass items,
// not the super -- tho could we just iterate up through the supers getting
// their lists to build the full list for each class? (e.g., for duplicate,
// persistance, or runtime diagnostic property editors)
// OH: we also need to build the bitfield for the enclosing class:
// the runtime-constant bit-mask representing all the properties
// handled by this class / subclass of LWComponent
}
private static final LWComponent EmptyStyle = new LWComponent();
static final Property NO_SLOT_PROVIDED = EmptyStyle.mFillColor; // any slot will do
//private static final Property BAD_SLOT = EmptyStyle.mStrokeColor; // any (different) slot will do
/** If this isn't overriden to return non-null, getValue & setValue must be overriden to provide the setter/getter impl */
Property getSlot(TSubclass c) { return NO_SLOT_PROVIDED; }
boolean isSlotted(TSubclass c) { return getSlot(c) != NO_SLOT_PROVIDED; }
// If we wanted to get rid of the slot decl's in the key's (for those that use
// slots), we could, in our defult slot-using set/getValue, search all property
// objects in the LWComponent, and if any of them match our key, we know that's
// that slot, and if none of them do, then we have in internal error: coder
// should have impl'd set/getValue themselves.
/** non slot-based property keys can override this */
TValue getValue(TSubclass c) {
final Property propertySlot = getSlotSafely(c);
try {
if (propertySlot == NO_SLOT_PROVIDED) {
if (DEBUG.META)
Log.error(this + ";\n\tNo property, or: no slot, and getValue not overriden on client subclass:\n\t"
+ (c == null ? null : c.getClass()) + ";\n\t" + c, new Throwable());
else
Log.warn(c == null ? null : c.getClass() + "; has no property of type: " + this);
return null;
} else
return (TValue) propertySlot.get();
} catch (Throwable t) {
if (DEBUG.META)
tufts.Util.printStackTrace(new Throwable(t), this + ": property slot get() failed " + propertySlot);
else
Log.warn(this + ": property slot get() failed " + propertySlot + " " + t);
return DEBUG.Enabled ? (TValue) "<unsupported for this object>" : null;
//return null;
}
}
// The design of the below two methods, setValue and setValueWithContext, is such that either
// can be overridden depending on the needs of the Key implementation, but never BOTH.
// Also, the setValue(TSubclass, TValue) method can never be called directly, except by
// the single call made to it in setValueWithContext. This is so that the context
// argument can be completely ignored in simple Key implementations, and only made
// use of where needed.
/** non slot-based property keys can override this -- only override ONE of the two setValue methods -- note that this
particular method should NOT EVER be called directly -- only overridden */
// what we really want here is some kind of access modifier that make this protected for overriding, but private to calling
protected void setValue(TSubclass c, TValue value) {
setValueBySlot(c, value);
}
/** non slot-based property keys can override this -- only override ONE of the two setValue methods
* Override the 2-argument version unless the context is needed */
void setValueWithContext(TSubclass c, TValue value, Object context) {
// if this has NOT been overriden (the usual case), defer to the default non-context setValue, which will defer to slot
// based value-set if needed.
setValue(c, value); // this is the only place that the 2-arg setValue should ever be called
}
private final void setValueBySlot(TSubclass c, TValue value) {
final Property slot = getSlotSafely(c);
if (slot == null || slot == NO_SLOT_PROVIDED)
return;
if (value instanceof String) {
// If a String value comes in, this allows us to auto-parse it
slot.setFromString((String)value);
} else {
slot.set(value);
}
}
private Property getSlotSafely(TSubclass c) {
Property slot = null;
try {
slot = getSlot(c);
} catch (ClassCastException e) {
String msg = "Property not supported: " + this + " on\t" + c + " (getSlot failed; returned null)";
//tufts.Util.printStackTrace(e, msg);
Log.warn(msg + "; " + e);
return null;
} catch (Throwable t) {
tufts.Util.printStackTrace(new Throwable(t), this + ": bad slot? unimplemented get/setValue?");
return null;
}
//if (slot == NO_SLOT_PROVIDED) tufts.Util.printStackTrace(this + ": no slot provided");
return slot;
}
/** non slot-based property keys can override this */
String getStringValue(TSubclass c) {
final Property slot = getSlotSafely(c);
if (slot == NO_SLOT_PROVIDED || slot == null) {
// If there is no slot provided, we must get the value from the overridden
// getter, getValue.
Object typedValue = null;
try {
// Call the overriden getValue:
typedValue = getValue(c);
} catch (ClassCastException e) {
final String msg = "Property not supported(getStringValue): " + this + " on\t" + c;
if (DEBUG.META)
tufts.Util.printStackTrace(e, msg);
else
Log.warn(msg + "; " + e);
return DEBUG.Enabled ? "<unsupported for this object>" : null;
}
return typedValue == null ? null : typedValue.toString(); // produce something
// } else if (slot == null) {
// // If a slot was provided, but it failed, no sense in trying
// // the default getValue, which presumably wasn't overriden if
// // a slot was provided.
// //tufts.Util.printStackTrace(this + ": bad slot");
// return DEBUG.Enabled ? "<unsupported for this object>" : null;
} else
return slot.asString();
}
void setStringValue(TSubclass c, String stringValue) {
Property slot = getSlotSafely(c);
if (slot != NO_SLOT_PROVIDED) {
slot.setFromString(stringValue);
} else {
TValue v = getValue(c);
// handle a few special cases for standard java types, even if there's no slot (Property object) to parse the string
// This won't work if getValue returns null, as we'll have no class object to check for type information.
if (v instanceof String) stringSet(c, stringValue);
else if (v instanceof Integer) stringSet(c, Integer.valueOf(stringValue));
else if (v instanceof Long) stringSet(c, Long.valueOf(stringValue));
else if (v instanceof Float) stringSet(c, Float.valueOf(stringValue));
else if (v instanceof Double) stringSet(c, Double.valueOf(stringValue));
else if (v instanceof Boolean) stringSet(c, Boolean.valueOf(stringValue));
else
Log.error(this + ":setValue(" + stringValue + "); no slot provided for parsing string value", new Throwable("HERE"));
}
}
private final void stringSet(TSubclass c, Object v) {
setValueWithContext(c, (TValue) v, PROPERTY_SET_DEFAULT); // may want a seperate context here, e.g., PROPERTY_SET_FROM_STRING
}
/** @return true if was successful */
boolean setValueFromCSS(TSubclass c, String cssKey, String cssValue) {
final Property slot = getSlot(c);
if (slot == Key.NO_SLOT_PROVIDED) {
c.out("Can't auto-apply CSS Style property to non-slotted key: " + cssName + " -> " + this);
return false;
}
try {
slot.setFromCSS(cssName, cssValue);
System.err.println("applied value: " + slot);
return true;
} catch (Throwable t) {
System.err.println();
tufts.Util.printStackTrace(new Throwable(t), "failed to apply CSS key/value " + cssName + "=" + cssValue);
}
return false;
}
/** @return true if the value for this Key in LWComponent is equivalent to otherValue
* Override to provide non-standard equivalence.
* The default provided here uses Object.equals to compare the values.
*/
boolean valueEquals(TSubclass c, Object otherValue)
{
final TValue value = getValue(c);
return value == otherValue || (otherValue != null && otherValue.equals(value));
}
void copyValue(TSubclass source, TSubclass target)
{
if (!source.supportsProperty(this)) {
if (DEBUG.STYLE && DEBUG.META) System.err.println(" COPY-VALUE: " + this + "; source doesn't support this property; " + source);
} else if (!target.supportsProperty(this)) {
if (DEBUG.STYLE && DEBUG.META) System.err.println(" COPY-VALUE: " + this + "; target doesn't support this property; " + target);
} else {
final TValue newValue = getValue(source);
final TValue oldValue = getValue(target);
if (newValue != oldValue && (newValue == null || !newValue.equals(oldValue))) {
if (DEBUG.STYLE) System.out.format(" COPY-VALUE: %s %s%-15s%s %-40s -> %s over (%s)\n",
source,
TERM_PURPLE,
this.name,
TERM_CLEAR,
//"(" + newValue + ")",
Util.tags(getStringValue(source)),
target,
oldValue);
setValueWithContext(target, newValue, PROPERTY_SET_DEFAULT); // may want a specific context here, e.g., PROPERTY_COPY
}
//if (DEBUG.STYLE) System.err.print(" COPY-VALUE: " + this + "(");
//if (DEBUG.STYLE) System.err.println(copyValue + ") -> " + target);
}
}
public String toString() { return name; } // must == name for now until tool panels handle new key objects (is this true yet?)
//public String toString() { return type + "{" + name + "}"; }
}
/**
* This class allows us to define an arbitrary property for a LWComponent, and define a default
* set of setters and getters that automatically handle stuff like undo and positing change
* notifications. It also allows us to easily attach meta-data to the property itself: e.g.,
* it's locked, it's overriding a parent style value, it's caching some related computed value,
* etc.
*/
protected abstract class Property<T> {
final Key key;
protected T value;
Property(Key key) {
this.key = key;
}
T get() { return value; }
public void setTo(T newValue) {
set(newValue);
}
boolean isChanged(T newValue)
{
if (this.value == newValue || (newValue != null && newValue.equals(this.value)))
return false;
else
return true;
}
void set(T newValue) {
//final Object old = get(); // if "get" actually does anything tho, this is a BAD idea; if needbe, create a "curValue"
if (!isChanged(newValue))
return;
final Object oldValue = this.value;
take(newValue);
onChange();
// RAISE CHANGE EVENT (for observers -- e.g., repaint, UndoManager, editors, etc)
// maybe: if (alive()) ?
LWComponent.this.notify(this.key, oldValue);
// note: if could handle event raising in Key class, we could make this class static.
// Tho then to bind to a particular property on a particular LWComponent, you'd need a
// Binding object. Do we actually bind directly to individual Property instance's
// anywhere? (Answer: seems no -- was just able to make this class protected instead of
// public) Could we possibly change class hierarchy such that LWComponent.Key is typed, and
// subclassed for the various types, and then do away with the Property class hierarchy
// entirely? -- SMF 2012
}
/** This JUST changes the stored value: no notifications of any kind will be triggered, no undo recorded. */
void take(T o) {
this.value = o;
if (DEBUG.TOOL) System.out.printf(" TAKING: %-30s -> %s\n", vtag(key, o, this), LWComponent.this);
}
/** impl's can override this to do something after the value has changed (after take() has been called),
* and before listeners have been notified */
void onChange() {}
void setFromString(String s) {
try {
setBy(s);
} catch (Throwable t) {
Log.error("bad value for " + this + ": [" + s + "] " + t);
}
}
void setFromCSS(String cssKey, String value) {
throw new UnsupportedOperationException(this + " unimplemented setFromCSS " + cssKey + " = " + value);
//VUE.Log.error("unimplemented setFromCSS " + cssKey + " = " + value);
}
void setBy(String fromValue) {
// Could get rid all of the setBy's (and then mayve even all the StyleProp subclasses!!)
// If we just had mapper class that took a type, a value, and returned a string (e.g., Font.class, Object value)
Log.error("unimplememnted: " + this + " setBy " + fromValue.getClass() + " " + fromValue);
}
/** override to provide an impl other than value.toString() */
String asString() {
return value == null ? null : value.toString();
}
/*
void setByUser(Object newValue) { // for tools. Actually, tools using generic setProperty right now...
out("SetByUser: " + key + " " + newValue);
set(newValue);
}
*/
/** used for debugging */
public String toString() {
return key + "[" + value + "]";
}
}
public class EnumProperty<T extends Enum> extends Property<T> {
EnumProperty(Key key, T defaultValue) {
super(key);
value = defaultValue;
//System.out.println("enum values: " + Arrays.asList(defaultValue.getClass().getEnumConstants()));
//System.out.println("enum test: " + Enum.valueOf(defaultValue.getClass(), "DASH1"));
}
void setBy(String s) {
// note: value can never be null, or we'll need to store the Enum class reference elsewhere
// (e.g., in the Key -- better there anyway, where we could provide a generic "values"
// to list the supported values)
set((T) Enum.valueOf(value.getClass(), s.trim()));
}
}
private static final String _DefaultString = "";
public class StringProperty extends Property<java.lang.String> {
StringProperty(Key key) {
super(key);
value = _DefaultString;
}
void setBy(String s) { set(s); }
}
public class BooleanProperty extends Property<java.lang.Boolean> {
BooleanProperty(Key key, Boolean defaultValue) {
super(key);
value = defaultValue;
}
BooleanProperty(Key key) {
this(key, Boolean.FALSE);
}
void setBy(String s) { set(Boolean.valueOf(s)); }
}
abstract public class NumberProperty<T> extends Property<T> {
NumberProperty(Key key) { super(key); }
void setFromCSS(String cssKey, String value) {
if (value.endsWith("pt") || value.endsWith("px"))
setBy(value.substring(0, value.length()-2));
else
throw new IllegalArgumentException("unhandled CSS number conversion for [" + value + "]");
}
}
static class PropertyValueVeto extends RuntimeException {
PropertyValueVeto(String msg) {
super(msg);
}
}
private static final Integer _DefaultInteger = new Integer(0);
public class IntProperty extends NumberProperty<java.lang.Integer> {
IntProperty(Key key, Integer defaultValue) {
super(key);
value = defaultValue;
}
IntProperty(Key key) {
this(key, _DefaultInteger);
}
void setBy(String s) { set(new Integer(s)); }
}
private static final Float _DefaultFloat = new Float(0f);
public class FloatProperty extends NumberProperty<java.lang.Float> {
FloatProperty(Key key) {
super(key);
value = _DefaultFloat;
}
void setBy(String s) { set(new Float(s)); }
}
public class FontProperty extends Property<java.awt.Font> {
FontProperty(Key key) {
super(key);
value = VueConstants.FONT_DEFAULT;
}
final void setBy(String s) {
//check for underline
String p = s.substring(s.indexOf("-")+1,s.length());
p = p.substring(0,p.indexOf("-"));
if (p.endsWith("underline"))
{ //do something
LWComponent.this.mFontUnderline.set("underline");
s= s.replaceAll(p, p.substring(0,p.indexOf("underline")));
}
Font f = Font.decode(s);
set(f);
}
final String asString() {
//if (this.font == null || this.font == getParent().getFont())
//return null;
final Font font = get();
String strStyle;
if (font.isBold()) {
strStyle = font.isItalic() ? "bolditalic" : "bold";
} else {
strStyle = font.isItalic() ? "italic" : "plain";
}
if (LWComponent.this.mFontUnderline.get().equals("underline"))
strStyle = strStyle.concat("underline");
return font.getName() + "-" + strStyle + "-" + font.getSize();
}
}
/**
* Handles CSS font-style value "italic" ("normal", or anything else, has no effect as of yet)
* Also handles CSS font-weight value of "bold" (anything else is ignored for now)
* todo: no hook for font-weight yet, permits invalid CSS
*/
public class CSSFontStyleProperty extends IntProperty {
CSSFontStyleProperty(Key key) { super(key); }
void setFromCSS(String cssKey, String value) {
// todo: this ignoring the key, which will permit non-confomant CSS
if ("italic".equalsIgnoreCase(value))
set(java.awt.Font.ITALIC);
else if ("bold".equalsIgnoreCase(value))
set(java.awt.Font.BOLD);
else
set(0);
}
}
/*
public class CSSFontSizeProperty extends IntProperty {
CSSFontSizeProperty(Key key) { super(key); }
void setFromCSS(String cssKey, String value) {
if (value.endsWith("pt"))
setBy(value.substring(0, value.length()-2));
else
throw new IllegalArgumentException("unhandled CSS font size [" + value + "]");
}
}
*/
public class CSSFontFamilyProperty extends StringProperty {
CSSFontFamilyProperty(Key key) { super(key); }
void setFromCSS(String cssKey, String value) {
// no translation needed for now: just use the raw name -- if it's a preference list tho, we'll need to handle it
setBy(value);
}
}
public class ColorProperty extends Property<java.awt.Color> {
private static final short ALPHA_NOT_PERMITTED = Short.MIN_VALUE;
private static final short NO_ALPHA_SET = -1;
private short fixedAlpha = NO_ALPHA_SET;
ColorProperty(Key key) { super(key); }
ColorProperty(Key key, Color defaultValue) {
this(key);
this.value = defaultValue;
}
public boolean isTransparent() {
return value == null || value.getAlpha() == 0;
}
public boolean isTranslucent() {
return value == null || value.getAlpha() != 0xFF;
}
void setAllowAlpha(boolean allow) {
if (allow)
fixedAlpha = NO_ALPHA_SET;
else
fixedAlpha = ALPHA_NOT_PERMITTED;
}
/** alpha should be in the range 0-255 */
void setFixedAlpha(int alpha) {
if (alpha > 255)
alpha = 255;
else if (alpha < 0)
alpha = 0;
fixedAlpha = (short) alpha;
//out("SET FIXED ALPHA " + fixedAlpha);
}
@Override
void set(Color newColor) {
if (fixedAlpha < 0) {
// the common case
super.set(newColor);
} else {
if (value == newColor)
return;
// enforce the fixed alpha on any incoming color:
if (newColor != null && newColor.getAlpha() != fixedAlpha && newColor.getAlpha() != 0) {
//out("COLOR VALUE: " + newColor + " " + ColorToString(newColor) + " alpha=" + newColor.getAlpha());
newColor = new Color((newColor.getRGB() & 0xFFFFFF) + (fixedAlpha << 24), true);
//out("used fixed alpha " + fixedAlpha + " producing " + newColor + " alpha=" + newColor.getAlpha()
//+ " " + ColorToString(newColor));
}
super.set(newColor);
}
}
@Override
void take(Color c) {
if (fixedAlpha < NO_ALPHA_SET && (c == null || c.getAlpha() != 0xFF))
throw new PropertyValueVeto(key + "; color with translucence: "
+ c
+ " alpha=" + c.getAlpha()
+ " not allowed on " + LWComponent.this);
// if (LWComponent.this instanceof LWNode)
// super.take(c == null ? null : new Color(c.getRGB() + (fixedAlpha << 24), true));
// //super.take(c == null ? null : new Color(c.getRGB() + ((128 & 0xFF) << 24), true));
// //super.take(c == null ? null : new Color(c.getRGB() + 0x20000000, true));
// else
super.take(c);
}
@Override
void setBy(String s) {
set(StringToColor(s));
}
@Override
void setFromCSS(String key, String value) {
// todo: CSS Style object could include the already instanced Color object
// we ignore key: assume that whatever it is is a color value
setBy(value);
}
/** @return a value between 0.0 and 1.0 representing brightness: the saturation % of the strongest channel
* e.g.: white returns 1, black returns 0
*/
public float brightness() {
return Util.brightness(value);
}
// dynamic version not workng
// ///** @return the color, but with 50% alpha (half transparent) */
// public final Color getWithAlpha(float alpha) {
// return new Color(value.getRGB() + (((byte)(alpha*256)) << 6), true);
// //return new Color(value.getRGB() + 0x80000000, true);
// }
public boolean equals(Color c) {
return value == c || (c != null && c.equals(value));
}
String asString() {
return ColorToString(get());
}
}
public static Color StringToColor(final String s)
{
if (s.trim().length() < 1)
return null;
Color c = null;
try {
c = VueResources.parseColor(s);
} catch (NumberFormatException e) {
tufts.Util.printStackTrace(new Throwable(e), "LWComponent.StringToColor[" + s + "]");
}
return c;
}
private static String ColorToDebugString(final Color c) {
final String s = ColorToString(c);
if (s == null)
return "#null00";
else
return s;
}
public static String ColorToString(final Color c)
{
// if null, or no hue and no alpha, return null
//if (c == null || ((c.getRGB() & 0xFFFFFF) == 0 && c.getAlpha() == 255))
if (c == null)
return null;
if (c.getAlpha() == 255) // opaque: only bother to save hue info
return String.format("#%06X", c.getRGB() & 0xFFFFFF);
else
return String.format("#%08X", c.getRGB());
}
public enum Alignment { LEFT, CENTER, RIGHT }
public static final Key KEY_FillColor = new Key("fill.color", "background") { final Property getSlot(LWComponent c) { return c.mFillColor; } };
public static final Key KEY_TextColor = new Key("text.color", "font-color") { final Property getSlot(LWComponent c) { return c.mTextColor; } };
public static final Key KEY_StrokeColor = new Key("stroke.color", "border-color") { final Property getSlot(LWComponent c) { return c.mStrokeColor; } };
//public static final Key KEY_StrokeStyle = new Key("stroke.style", "border-style") { final Property getSlot(LWComponent c) { return null; } };
public static final Key KEY_StrokeWidth = new Key("stroke.width", "stroke-width") { final Property getSlot(LWComponent c) { return c.mStrokeWidth; } };
public static final Key KEY_StrokeStyle = new Key<LWComponent,StrokeStyle>
("stroke.style", KeyType.STYLE) { final Property getSlot(LWComponent c) { return c.mStrokeStyle; } };
public static final Key KEY_Alignment = new Key<LWComponent,Alignment>
("alignment", KeyType.STYLE) { final Property getSlot(LWComponent c) { return c.mAlignment; } };
/* font.size: point size for font */
/* font.style: @See java.awt.Font 0x0=Plain, 0x1=Bold On, 0x2=Italic On */
/* font.name: family name of the font */
/** Aggregate font key, which represents the combination of it's three sub-properties */
public static final Key KEY_Font = new Key("font", KeyType.STYLE) { final Property getSlot(LWComponent c) { return c.mFont; } };
public static final Key KEY_FontSize = new Key("font.size", KeyType.SUB_STYLE) { final Property getSlot(LWComponent c) { return c.mFontSize; } };
public static final Key KEY_FontStyle = new Key("font.style", KeyType.SUB_STYLE) { final Property getSlot(LWComponent c) { return c.mFontStyle; } };
public static final Key KEY_FontUnderline = new Key("font.underline", KeyType.SUB_STYLE) { final Property getSlot(LWComponent c) { return c.mFontUnderline; } };
public static final Key KEY_FontName = new Key("font.name", KeyType.SUB_STYLE) { final Property getSlot(LWComponent c) { return c.mFontName; } };
public static final Key KEY_Collapsed =
new Key<LWComponent,Boolean>("collapsed") {
@Override public void setValue(LWComponent c, Boolean collapsed) {
c.setCollapsed(collapsed);
}
@Override public Boolean getValue(LWComponent c) {
return c.isCollapsed() ? Boolean.TRUE : Boolean.FALSE;
}
};
public static final Key KEY_Created =
new Key<LWComponent,Long>("created") {
@Override public Long getValue(LWComponent c) {
return c.getCreated();
}
@Override public String getStringValue(LWComponent c) {
return new Date(c.getCreated()).toString();
}
};
public final ColorProperty mFillColor = new ColorProperty(KEY_FillColor);
public final ColorProperty mTextColor = new ColorProperty(KEY_TextColor, java.awt.Color.black) {
//{ color = java.awt.Color.black; } // default value
void onChange() {
if (labelBox != null)
labelBox.copyStyle(LWComponent.this); // todo better: handle thru style.textColor notification?
}
};
public final ColorProperty mStrokeColor = new ColorProperty(KEY_StrokeColor, java.awt.Color.darkGray);
public final FloatProperty mStrokeWidth = new FloatProperty(KEY_StrokeWidth) { void onChange() { rebuildStroke(); }};
public final EnumProperty<Alignment> mAlignment = new EnumProperty(KEY_Alignment, Alignment.LEFT) {
void onChange() { layout(KEY_Alignment); }
};
public final EnumProperty<StrokeStyle> mStrokeStyle = new EnumProperty(KEY_StrokeStyle, StrokeStyle.SOLID) { void onChange() { rebuildStroke(); }};
public enum StrokeStyle {
SOLID (1,0),
DOTTED (1,1),
DASHED (2,2),
DASH2 (3,2),
DASH3 (5,3);
private final float[] dashPattern = new float[2];
StrokeStyle(float dashOn, float dashOff) {
dashPattern[0] = dashOn; // pixels on (drawn)
dashPattern[1] = dashOff; // pixels off (whitespace)
}
public BasicStroke makeStroke(double width) {
return makeStroke((float) width);
}
public BasicStroke makeStroke(float width) {
if (this == SOLID)
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
//return new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
//return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND);
else
return new BasicStroke(width
, BasicStroke.CAP_BUTT // anything else will mess with the dash pattern
, BasicStroke.JOIN_BEVEL
, 10f // miter-limit
//, 0f // miter-limit
, dashPattern
, 0f); // dash-phase (offset to start of pattern -- apparently pixels, not index)
}
// todo opt: better: could cache the strokes here for each dash pattern/size
}
private void rebuildStroke() {
final float width = mStrokeWidth.get();
if (width > 0)
this.stroke = mStrokeStyle.get().makeStroke(width);
else
this.stroke = STROKE_ZERO;
/*/ below code was broken in previous code. Node child layout does NOT
// appear to be taking into account total bounds with at the moment anyway...
// (Or was that just for Groups? No, those appear to be handling the full bounds change.)
// Also, want to make generic with a flag in Key if layout needed when
// the given property changes.
if (getParent() != null) {
// because stroke affects bounds-width, may need to re-layout parent
getParent().layout();
}
layout();*/
}
public final IntProperty mFontStyle = new CSSFontStyleProperty(KEY_FontStyle) { void onChange() { rebuildFont(); } };
public final IntProperty mFontSize = new IntProperty(KEY_FontSize) { void onChange() { rebuildFont(); } };
public final StringProperty mFontName = new CSSFontFamilyProperty(KEY_FontName) { void onChange() { rebuildFont(); } };
public final StringProperty mFontUnderline = new StringProperty(KEY_FontUnderline) {
boolean isChanged(String newValue) {
return true;
}
@Override void onChange() {
rebuildFont();
if (labelBox != null) {
labelBox.copyStyle(LWComponent.this);
layout(this.key); // could make this generic: add a key bit that says "layout needed on-change";
}
}
};
private boolean fontIsRebuilding; // todo: use a bit flag
private void rebuildFont() {
// This so at least for now we have backward compat with the old font property (esp. for tools & persistance)
fontIsRebuilding = true;
try {
Font f =new Font(mFontName.get(), mFontStyle.get(), mFontSize.get());
mFont.set(f);
} finally {
fontIsRebuilding = false;
}
}
public final FontProperty mFont = new FontProperty(KEY_Font) {
@Override void onChange() {
if (!fontIsRebuilding) {
final Font f = get();
mFontStyle.take(f.getStyle());
mFontSize.take(f.getSize());
mFontName.take(f.getName());
}
if (labelBox != null) {
labelBox.copyStyle(LWComponent.this);
layout(this.key); // could make this generic: add a key bit that says "layout needed on-change";
}
}
};
public static final String KEY_LabelFormat = "label.format";
public static final Key KEY_Label = new Key<LWComponent,String>("label", KeyType.DATA) {
@Override public void setValueWithContext(LWComponent c, String val, Object context) {
if (context == PROPERTY_SET_UNDO)
c.setLabelImpl(val, true, false);
else
c.setLabel(val);
}
@Override public String getValue(LWComponent c) { return c.getLabel(); }
};
public static final Key KEY_Notes = new Key<LWComponent,String>("notes", KeyType.DATA) {
@Override public void setValue(LWComponent c, String val) { c.setNotes(val); }
@Override public String getValue(LWComponent c) { return c.getNotes(); }
};
//===================================================================================================
//
// End of Key's and Properties
//
//===================================================================================================
// for debug
private static String vtag(Object key, Object val, Property p)
{
if (val == null) {
return key + "(null)";
} else if (val.getClass() == String.class) {
return key + "(\"" + val + "\")";
}
String typeName = val.getClass().getName();
String valType = typeName.substring(typeName.lastIndexOf('.') + 1);
String valRep = (p == null ? val.toString() : p.asString());
String extra = "";
//if (p != null) extra = val.toString();
//valType += "@" + Integer.toHexString(val.hashCode());
return key + " " + valType + "(" + valRep + ")" + extra + "";
}
// todo: this not yet implemented as actually persistant -- it
// meant to be picked out of client data at save time and be
// specially persisted
public static class PersistClientDataKey {
final String name;
public PersistClientDataKey(String s) { name = s; }
@Override public String toString() {
return name;
}
}
// maybe rename these store/fetchProperty
/** set a generic property in the model -- users of this API need to ensure the keys
* they're using are unique with respect to any other potential users of this API.
* Setting a property value to null removes the property. This is basically "cookies"
* for LWComponents at runtime.
*/
public void setClientData(Object key, Object o) {
if (mClientData == null) {
if (o == null) // storing null means remove value
return;
else
mClientData = new HashMap(2);
}
if (DEBUG.DATA) {
String keyName = key instanceof Class ? key.toString() : Util.tags(key);
out("setClientData: " + keyName + "=" + Util.tags(o));
}
if (o == null) {
mClientData.remove(key);
if (mClientData.size() == 0)
mClientData = null;
} else {
mClientData.put(key, o);
}
}
public <T> T putClientData(T o) {
setClientData(o.getClass(), o);
return o;
}
public Object getClientData(Object key) {
return mClientData == null ? null : mClientData.get(key);
}
public void setClientData(Class classKey, String subKey, Object o) {
setClientData(classKey.getName() + "/" + subKey, o);
}
public boolean hasClientData(Object o) {
return getClientData(o) != null;
}
public void flushAllClientData() {
if (mClientData != null) {
if (DEBUG.DATA && mClientData.size() > 0) {
Log.debug("flushing all client data;");
Util.dump(mClientData);
}
mClientData = null;
}
}
// public void setInstanceProperty(String subKey, Object o) {
// setClientProperty(o.getClass().getName() + "/" + subKey, o);
// }
// public void setInstanceProperty(Object o) {
// setClientProperty(o.getClass(), o);
// }
// todo: support client data via arbitrary ENUM values (e.g., with EnumMap, and maybe bits via an EnumSet)
/** convenience typing fetch when using a Class as a property key, that returns a value casted to the class type */
public <A> A getClientData(Class<A> classKey, String subKey) {
final Object o = getClientData(classKey.getName() + "/" + subKey);
if (classKey == Boolean.class && o == null)
return (A) Boolean.FALSE; // special hack for null == boolean false
else
return (A) o;
}
/** convenience typing fetch when using a Class as a property key, that returns a value casted to the class type */
public <A> A getClientData(Class<A> classKey) {
return (A) getClientData((Object)classKey);
}
/**
* Get the named property value from this component.
* @param key property key (see LWKey)
* @return object representing appropriate value, or null if none found (note: properties may be null also -- todo: fix)
*/
public Object getPropertyValue(final Object key)
{
if (key instanceof Key) {
// If getValue on the key was overriden, we may still need to trap an exception here
try {
return ((Key)key).getValue(this);
} catch (ClassCastException e) {
String msg = "Property not supported(getPropertyValue): " + key + " on " + this + " (returned null)";
if (DEBUG.META)
tufts.Util.printStackTrace(e, msg);
else
Log.warn(msg + "; " + e);
return null;
}
}
// Old property keys that don't make use of the Key class yet:
if (key == LWKey.Resource) return getResource();
if (key == LWKey.Location) return getLocation();
if (key == LWKey.Size) return new Size(this.width, this.height);
if (key == LWKey.Hidden) return isHidden() ? Boolean.TRUE : Boolean.FALSE;
Log.warn(this + " getPropertyValue; unsupported property [" + key + "] (returning null)");
if (key == null) Util.printStackTrace("key was null");
//throw new RuntimeException("Unknown property key[" + key + "]");
return null;
}
public static final Object PROPERTY_SET_DEFAULT = "default";
public static final Object PROPERTY_SET_UNDO = "undo";
//public static final Object PROPERTY_SET_USER = "propertyUser";
void undoProperty(Object key, Object val) {
setPropertyImpl(key, val, PROPERTY_SET_UNDO);
}
public final void setProperty(final Object key, final Object val) {
setPropertyImpl(key, val, PROPERTY_SET_DEFAULT);
}
protected void setPropertyImpl(final Object key, final Object val, final Object context)
{
if (DEBUG.TOOL||DEBUG.UNDO) Log.debug("setPropertyImpl[" + context + "] " + this + " " + vtag(key, val, null));
if (key instanceof Key) {
final Key k = (Key) key;
k.setValueWithContext(this, val, context);
}
// Old property keys that don't make use of the Key class yet:
//else if (key == LWKey.Hidden) setHidden( ((Boolean)val).booleanValue());
else if (key == KEY_LabelFormat) setLabelFormat((String)val);
else if (key == LWKey.DataUpdate) setDataMap((MetaMap) val);
else if (key == LWKey.Scale) setScale((Double) val);
else if (key == LWKey.Resource) setResource( (Resource) val);
else if (key == LWKey.Location) {
// This is a bit of a hack, in that we're relying on the fact that the only
// thing to call setProperty with a Location key right now is the
// UndoManager. In any case, on undo, we do NOT want to additionally make
// mapLocationChanged calls on all descendents (for absolute map location
// objects; e.g. LWLink's). Location changes as a result of these calls
// were already recorded as events and will be undone on their own.
final Point2D.Float loc = (Point2D.Float) val;
setLocation(loc.x, loc.y, this, false);
//setLocation( (Point2D) val);
}
else if (key == LWKey.Size) {
Size s = (Size) val;
setSize(s.width, s.height);
}
else if (key == LWKey.Frame) {
Rectangle2D.Float r = (Rectangle2D.Float) val;
setFrame(r.x, r.y, r.width, r.height);
}
// else if (key == LWKey.Hidden) {
// // would need HIDE_CAUSE
// //setHidden(((Boolean)val).booleanValue());
// Log.debug("setProperty " + key + "=" + val + " on " + this);
// }
else {
//out("setProperty: unknown key [" + key + "] with value [" + val + "]");
tufts.Util.printStackTrace("FYI: Unhandled Property key: " + key.getClass() + "[" + key + "] with value [" + val + "]");
}
}
/**
* This is used during duplication of group's of LWComponent's
* (e.g., a random selection, or a set of children, or an entire map),
* to reconnect links within the group after duplication, and
* passing flags into the dupe context.
*/
public static class LinkPatcher {
private java.util.Map<LWComponent,LWComponent> mCopies = new java.util.IdentityHashMap();
private java.util.Map<LWComponent,LWComponent> mOriginals = new java.util.IdentityHashMap();
public LinkPatcher() {
if (DEBUG.DND) Log.debug("LinkPatcher: created");
}
public void reset() {
mCopies.clear();
mOriginals.clear();
}
public void track(LWComponent original, LWComponent copy)
{
if (DEBUG.DND && DEBUG.META) Log.debug("LinkPatcher: tracking " + copy);
mCopies.put(original, copy);
mOriginals.put(copy, original);
}
//public Collection getCopies() { return mCopies.values(); }
public void reconnectLinks() {
// Find all LWLink instances in the set of copied
// objects, and fix their endpoint pointers to
// point to the right object within the copied set.
for (LWComponent c : mCopies.values()) {
if (!(c instanceof LWLink))
continue;
final LWLink linkCopy = (LWLink) c;
final LWLink linkOriginal = (LWLink) mOriginals.get(linkCopy);
final LWComponent headCopy = mCopies.get(linkOriginal.getHead());
final LWComponent tailCopy = mCopies.get(linkOriginal.getTail());
if (DEBUG.DND)
Log.debug("LinkPatcher: reconnecting " + linkCopy + " endpoints:"
+ "\n\t" + headCopy
+ "\n\t" + tailCopy
);
linkCopy.setHead(headCopy);
linkCopy.setTail(tailCopy);
}
}
}
public static class CopyContext {
final boolean dupeChildren;
LinkPatcher patcher;
CopyContext() { this(true); }
CopyContext(boolean dupeChildren) {
this.dupeChildren = dupeChildren;
}
CopyContext(LinkPatcher lp, boolean dupeChildren) {
this.patcher = lp;
this.dupeChildren = dupeChildren;
}
void reset() {
if (patcher != null)
patcher.reset();
}
void complete() {
if (patcher != null)
patcher.reconnectLinks();
}
}
protected void copySupportedProperties(LWComponent c) {
mSupportedPropertyKeys = c.mSupportedPropertyKeys;
}
public boolean canDuplicate() {
return true;
}
public LWComponent duplicate() {
return duplicate(new CopyContext());
}
/**
* Create a component with duplicate content & style. Does not duplicate any links
* to this component, and leaves it an unparented orphan. Leaving the parent null
* is important until we know what we're doing with the duplicate. E.g., it may
* just sit in a scratch buffer and used as a down-stream duplicating source
* for pasting, and never be added to the model anywhere.
*
* @param CopyContext may be null. If not, it's used when duplicating group's of
* objects containing links that need to be reconnected at the end of the duplicate.
*/
public LWComponent duplicate(CopyContext cc)
{
final LWComponent c;
try {
c = getClass().newInstance();
} catch (Throwable t) {
Log.error("duplicate " + getClass(), t);
return null;
}
return duplicateTo(c, cc);
}
/**
* Provided for subclass impl's that need to support final members, which can use a
* pre-constructed empty object in their override of duplicate v.s. relying on the default use
* of newInstance via calls to super.duplicate. Technically, this also permits any subclass of
* instance of LWComponent to "duplicate" an instance of different subclass, and only the
* compatible properties will copy themselves over.
*/
protected <Ts extends LWComponent> Ts duplicateTo(Ts c, CopyContext cc)
{
c.setFlag(Flag.DUPLICATING);
c.mXMLRestoreUnderway = true; // todo: this flag really "initUnderway" -- need to double-check all our semantics tho...
c.copySupportedProperties(this);
c.x = this.x;
c.y = this.y;
c.width = this.width;
c.height = this.height;
c.scale = this.scale;
c.stroke = this.stroke; // cached info only
c.copyMetaData(this);
c.copyStyle(this); // this copies over all compatiable Property values
c.setAutoSized(isAutoSized()); // may be sensitive to order of operations during init
if (c instanceof LWText) {
c.label=this.label;
((LWText)c).getRichLabelBox().setText(((LWText)this).getRichLabelBox().getRichText());
} else {
c.setLabelImpl(this.label, true, false);
c.getLabelBox().setSize(getLabelBox().getSize());
}
if (hasResource())
c.setResource(getResource());
if (hasNotes())
c.setNotes(getNotes());
if (cc.patcher != null)
cc.patcher.track(this, c);
c.mXMLRestoreUnderway = false;
c.layout("duplicate");
c.clearFlag(Flag.DUPLICATING);
return c;
}
protected void copyMetaData(final LWComponent source)
{
if (source.metadataList != null) {
// duplicate original style meta-data list
final List<VueMetadataElement> srcVMD = source.getMetadataList().getMetadata();
final List<VueMetadataElement> targetVMD = this.getMetadataList().getMetadata();
for (VueMetadataElement vme : srcVMD)
targetVMD.add(vme);
}
// duplicate data-set data
if (source.mDataMap != null)
mDataMap = source.mDataMap.clone();
}
protected boolean isPresentationContext() {
if (true) return false;// turned off for now
if (parent == null)
return false; // this means presentation nodes will report wrong sizes during restores...
else
return parent.isPresentationContext();
}
protected final void ensureID(LWComponent c) {
ensureID(c, true);
}
/**
* Make sure this LWComponent has an ID -- will have an effect on
* on any brand new LWComponent exactly once per VM instance.
* @param descendents - if true, will also ensure all descendents
*/
protected final void ensureID(LWComponent c, boolean descendents)
{
if (c.getID() == null) {
String id = getNextUniqueID();
// no ID may be available if we're an orphan: it will be
// patched up when we eventually get added to to a map
if (id != null)
c.setID(id);
}
// if (descendents) {
// for (LWComponent child : c.getAllDescendents(ChildKind.ANY))
// ensureID(child, false);
// }
// Not safe: tho shouldn't happen, if any parent<->child loops make their way into the hierarchy, we can stack overflow here
// [doesn't matter: getAllDescendents will just stack-overflow instead]
if (descendents) {
for (LWComponent child : c.getChildList()) {
ensureID(child);
}
}
}
protected String getNextUniqueID()
{
if (getParent() == null) {
//throw new IllegalStateException("LWComponent has null parent; needs a parent instance subclassed from LWContainer that implements getNextUniqueID: " + this);
//if (DEBUG.PARENTING) tufts.Util.printStackTrace("getNextUniqueID: returning null for current orphan " + this);
if (DEBUG.PARENTING) out("getNextUniqueID: returning null for current orphan");
return null;
} else
return getParent().getNextUniqueID();
}
//private static int MapDepth;
public LWMap getMap() {
if (parent == null) {
return null;
} else {
// if (++MapDepth >= 64) { // DEBUG
// Util.printStackTrace("PARENT LOOP at depth " + MapDepth);
// System.err.println("LWC: " + this);
// return null;
// }
// final LWMap m = parent.getMap();
// MapDepth--;
// return m;
return parent.getMap();
}
}
public UndoManager getUndoManager() {
final LWMap map = getMap();
if (map == null)
return null;
else
return map.getUndoManager();
}
protected void addCleanupTask(Runnable task) {
addCleanupTask(task, this);
//addCleanupTask(task, this, null);
}
// protected void addCleanupTask(Runnable task, Object taskKey, Object srcMsg) {
protected void addCleanupTask(Runnable task, Object taskKey) {
final UndoManager um = getUndoManager();
if (um != null) {
if (um.isUndoing()) {
if (DEBUG.WORK || DEBUG.UNDO) System.out.println("Ignoring cleanup task during undo: " + task + " for " + this);
} else if (um.hasCleanupTask(taskKey)) {
if (DEBUG.WORK || DEBUG.UNDO) System.out.println("Ignoring duplicate cleanup task: " + task + " for " + this);
} else {
boolean debug = DEBUG.WORK || DEBUG.UNDO;
if (isDeleted()) {
Util.printStackTrace("warning: adding cleanup task when deleted");
debug = true;
}
// if (debug) {
// System.out.println(TERM_RED + "ADDING CLEANUP TASK: " + task
// + (srcMsg==null?"":("on " + srcMsg))
// + (task == this ? "" : (" for " + this))
// + TERM_CLEAR);
// }
um.addCleanupTask(this, task);
}
}
}
public UserMapType getUserMapType() { throw new UnsupportedOperationException("deprecated"); }
public static final String EnumeratedValueKey = "@ValueOf";
//public void setDataInstanceValue(String key, Object value) {
public void setDataInstanceValue(tufts.vue.ds.Field field, Object value) {
//getMetadataList().add(key, value.toString());
final String key = field.getName();
getMetadataList().add(key, value.toString());
getDataMap().put(key, value);
getDataMap().setSchema(field.getSchema());
getDataMap().put(EnumeratedValueKey, field.getName());
//getDataMap().put("@Schema", field.getSchema());
}
public TableBag getDataTable() {
return mDataMap;
}
private MetaMap getDataMap() {
if (mDataMap == null) {
mDataMap = new MetaMap();
}
return mDataMap;
}
/** for castor peristance */
public MetaMap getPersistDataMap() {
if (mXMLRestoreUnderway)
return getDataMap();
else
return mDataMap; // if null on save, nothing to persist
}
/** for castor peristance */
public void setPersistDataMap(MetaMap dataMap) {
mDataMap = dataMap;
}
/** replace ALL data on this node at once, generating events for undo */
public void setDataMap(MetaMap dataMap) {
if (mDataMap == dataMap)
return;
Object old = this.mDataMap;
mDataMap = dataMap;
notify(LWKey.DataUpdate, old);
if (mLabelFormat != null) {
setLabelImpl(fillLabelFormat(mLabelFormat), true, false);
}
}
// public Collection<Map.Entry<String,Object>> getPersistDataMap() {
// if (mXMLRestoreUnderway)
// return getDataMap().entries();
// else if (mDataMap != null)
// return mDataMap.entries();
// else
// return null;
// }
//public void addDataValues(final Iterable<Map.Entry<String,String>> entries) {
// public void addDataValues(final Iterable<Map.Entry> entries) {
// getMetadataList().add(entries);
// getDataMap().putAllStrings(entries);
// }
/** used for new node creation: todo -- get rid of this when we get rid of getMetadataList() */
public void takeAllDataValues(MetaMap map) {
setPersistDataMap(map);
//----------------------------------------------------------------------------------------
// TODO: GET RID OF THIS DUPLICATION. This is done so that these can be UI editable and so
// RDFIndex will index these, making them searchable. Of course, only the COPIES are
// editable -- the originals stay the same. But adding these to the RDFIndex directly from
// the mDataMap would be trivial to add in RDFIndex.java, and do we really need/want to be
// able to edit them? Of course, what we really want is to get rid of the
// VueMetaDataElement and related MetadataList classes completely.
// ----------------------------------------------------------------------------------------
getMetadataList().add(map.entries()); // duplicate in old meta-data for now
}
public void addDataValue(String key, String value) {
// TODO PERFORMANCE: GET RID OF THIS:
getMetadataList().add(key, value);
// just leave this:
getDataMap().add(key, value);
}
public String getDataValue(String key) {
if (mDataMap == null)
return null;
return mDataMap.getString(key);
// VueMetadataElement vme = getMetadataList().get(key);
// return vme == null ? null : vme.getValue();
}
// TODO: rename all these getDataValue* to getSingleValue*
public String getDataValueFieldName() {
if (mDataMap == null)
return null;
String fieldName = mDataMap.getString(EnumeratedValueKey);
// //-----------------------------------------------------------------------------
// // backward compat before @schema.field stored, and
// // only @schema was stored. The first entry should always be
// // the schmatic field. todo: remove this eventually
// if (fieldName == null && isDataValueNode()) {
// final Map.Entry firstEntry = mDataMap.entries().iterator().next();
// fieldName = firstEntry.getKey().toString();
// }
// //-----------------------------------------------------------------------------
return fieldName;
}
/** @return the Field if this a single-value field node, null otherwise */
public Field getDataValueField() {
if (mDataMap == null || mDataMap.getSchema() == null)
return null;
String fieldName = getDataValueFieldName();
if (fieldName != null)
return mDataMap.getSchema().getField(fieldName);
else
return null;
}
/** @return true if the data-set data for this node represents a SINGLE VALUE from a field (e.g., one of an enumeration)
* Should always return the opposite of isDataRow */
public boolean isDataValueNode() {
return mDataMap != null && mDataMap.hasKey(EnumeratedValueKey);
}
/** @return true if this is a single value data node of the given name from *any* schema */
public boolean isDataValueNode(String name) {
return name.equals(getDataValueFieldName());
}
/** @return true if this is a single value data node for the given Field in it's Schema */
public boolean isDataValueNode(Field field) {
return getDataSchema() == field.getSchema() && field.getName().equals(getDataValueFieldName());
}
/**
* @return true if this is a data-row node from the given schema.
* todo: schema checking is currently weak -- only checks for key field
*/
public boolean isDataRow(Schema schema) {
//return hasDataKey(schema.getKeyField().getName()) && !isDataValueNode();
return getDataSchema() == schema && !isDataValueNode();
}
public boolean isDataRowNode() {
return getDataSchema() != null && !isDataValueNode();
}
public MetaMap getRawData() {
//if (mDataMap == null) Util.printStackTrace("NULLDATA");
return mDataMap;
}
public final boolean isDataNode() {
return mDataMap != null;
}
public boolean hasDataKey(String key) {
return mDataMap != null && mDataMap.hasKey(key);
}
public Schema getDataSchema() {
if (mDataMap != null)
return mDataMap.getSchema();
else
return null;
}
/** @return true if a schema-handle was turned into a live schema reference */
boolean validateSchemaReference() {
final MetaMap data = getRawData();
if (data == null)
return false;
final Schema schema = data.getSchema();
if (schema != null) {
Schema liveSchema = Schema.lookupAuthority(schema);
if (schema != liveSchema) {
data.setSchema(liveSchema);
if (DEBUG.DATA) Log.debug("updated schema " + this);
return true;
}
}
return false;
}
/** @return null -- this is only needed for LWMap, but is implemented here to force
* the order of persistance based on the castor mapping, so schemas can persist
* before nodes in the LWMap, which overrides this to return schemas included in the
* map. If this was only declared in the mapping file as an LWMap persistance item,
* it would persist after all LWComponent mappings (LWMap subclasses LWComponent).
*/
public Collection<tufts.vue.ds.Schema> getIncludedSchemas() {
return null;
}
public boolean hasDataValue(String key, CharSequence value) {
return mDataMap != null && mDataMap.hasEntry(key, value);
//return isSchematicFieldNode() && mDataMap.containsEntry(key, value);
// if (mDataMap == null)
// return false;
// return mDataMap.containsKey("@Schema") && mDataMap.containsEntry(key, value);
// // VueMetadataElement vme = getMetadataList().get(key);
// // return vme == null ? false : value.equals(vme.getValue());
}
// public void containsMetaData(String key, Object value) {
// getMetadataList().add(key, value.toString());
// }
/**
* Metadata List for use with RDF Index It is sufficient for the minimal RDF functionality to
* be able to retrieve this list from the LWComponent using this method and add elements
* directly to the list as needed. LWComponent may choose to create notifications/modifcations
* for any data added directly through LWComponent itself in future.
**/
public MetadataList getMetadataList() {
if (metadataList == null)
metadataList = new MetadataList();
return metadataList;
}
public void setMetadataList(MetadataList list) {
metadataList = list;
}
public void setXMLmetadataList(MetadataList list) {
setMetadataList(list);
}
public MetadataList getXMLmetadataList() {
if (mXMLRestoreUnderway) {
return getMetadataList();
} else {
// persist underway:
if (metadataList != null && metadataList.isEmpty())
return null;
else
return metadataList;
}
}
/** see edu.tufts.vue.metadata.VueMetadataElement for metadata types **/
public boolean hasMetaData(int type) {
if (metadataList != null)
return metadataList.hasMetadata(type);
else
return false;
// return ( (metadataList != null) && (getMetaDataAsHTML().length() > 0) );
}
public boolean hasMetaData() {
return hasMetaData(edu.tufts.vue.metadata.VueMetadataElement.CATEGORY);
}
public String getMetaDataAsHTML() {
return getMetaDataAsHTML(edu.tufts.vue.metadata.VueMetadataElement.CATEGORY);
}
/** see edu.tufts.vue.metadata.VueMetadataElement for metadata types */
public String getMetaDataAsHTML(int type) {
if (metadataList != null)
return metadataList.getMetadataAsHTML(type);
else
return "";
}
/**
* @return true if should not be drawn due to a currently applied filter, false if not.
* Note that FILTERING is diffrent than HIDING via a HideCause. A hidden node
* hides all of its children, but a filtered not does not (which can create
* some strange visual situations, but there you have it).
**/
public boolean isFiltered() {
return hasFlag(Flag.FILTERED);
}
/**
* [This sets the flag for the component so that it is either
* hidden or visible based on a match to the active LWCFilter.]
* 2012: are LWCFilters still used?
**/
public void setFiltered(boolean filtered) {
//if (DEBUG.SEARCH&&DEBUG.TEST) Log.debug("setFiltered " + filtered + "; " + this);
setFlag(Flag.FILTERED, filtered);
}
// protected void setFilterBits(int bits) {
// final boolean wasFiltered = isFiltered()
// mHideBits = bits;
// if (wasHidden != isHidden())
// notify(LWKey.Hidden);
// //notify(LWKey.Hidden, wasHidden); // if we need it to be undoable
// }
protected boolean alive() {
// "internal" objects should always report events (e.g., special styles, such as data-styles)
return parent != null
|| hasFlag(Flag.INTERNAL); // re-enabled for data-style records reporting thier changes to the DataTree
}
/** This should only be called once when added to the map, and on deserializations */
public void setCreated(final long time) {
//Log.debug(String.format("setCreated %s; %s", new Date(time), this));
if (mCreated != 0 && alive()) {
Log.warn("setCreated erasing " + mCreated + "=" + new Date(mCreated)
+ " with: " + time + "=" + new Date(time) + "; " + this);
}
mCreated = time;
}
public long getCreated() {
return mCreated;
}
/**
* Called during restore from presistance, or when newly added to a container.
* Must be called at some point before any attempt to persist, with a unique
* identifier within the entire LWMap. This is how components are referenced
* in the persisted data.
*/
public void setID(String ID)
{
if (this.ID != null)
throw new IllegalStateException("Can't set ID to [" + ID + "], already set on " + this);
//System.out.println("setID [" + ID + "] on " + this);
this.ID = ID;
// special case: if undo of add of any component that was brand new, this is
// a new component creation, and to undo it is actually a delete.
// UndoManager handles the hierarchy end of this, but we need this here
// to differentiate hierarchy events that are just reparentings from
// new creation events.
if (!mXMLRestoreUnderway) {
// setting the creation time when the ID is set is appropriate because the
// ID is set whenever a node newly joins a map: e.g., a duplicate of a node
// will have it's ID re-set when it's added to a new map. This is often
// still the fall-back though: LWContainer will apply the exact same stamp
// to collections of nodes that are newly added at the same time.
if (mCreated == 0) {
if (DEBUG.WORK) Log.debug("fallback timestamp: " + this);
setCreated(System.currentTimeMillis());
}
notifyForce(LWKey.Created, new Undoable() {
void undo() {
// parent may already have deleted it for us, so only delete if need be
// todo performance: force parents to always handle this so can skip creating this event
// (has impact when creating handling thousands of nodes)
if (!isDeleted())
removeFromModel();
}} );
}
}
// /** set the ID string, no questions asked */
// protected void takeID(String ID) {
// this.ID = ID;
// }
public void setLabel(String label)
{
setLabelImpl(label, true, true);
}
// todo: rename / use a package private setLabelImpl
void setLabel0(String newLabel, boolean setDocument) {
setLabelImpl(newLabel, setDocument, true);
}
/** for persistance */
public String getLabelFormat() {
return mLabelFormat;
}
/** for persistance -- will not update the label */
public void setLabelFormat(String s) {
if (s == mLabelFormat || (s != null && s.equals(mLabelFormat)))
return;
final Object old = mLabelFormat;
mLabelFormat = s;
if (alive()) notify(KEY_LabelFormat, old);
//if (alive()) notify("label.format", new Undoable(oldFormat) { void undo() { setLabelFormat(oldFormat); }} );
}
public void setLabelTemplate(String s) {
setLabelImpl(s, false, false);
}
private static boolean isDataFormatString(String s) {
return s != null && s.indexOf('$') >= 0;
// todo: check with full regex: e.g: \$\{.+\}
// however, it's okay to over-match, as replacements that can't be understood are left as-is
}
/**
* @param setDocument -- we're called by TextBox after document edit with setDocument=false,
* so we don't attempt to re-update the TextBox, which has just been
* updated.
*
* @param allowFillData -- called with allowFillData=false if we don't
* want to actually do a data-fill and just want to leave the
* label as the actual format (e.g. we just computed filled data
* and now want to set the label for real, or this is an undo).
*/
protected void setLabelImpl
(String newLabel,
final boolean setDocument,
final boolean allowDataFill)
{
if (DEBUG.TEXT || DEBUG.DATA) out("setLabelImpl " + Util.tags(newLabel) + " setDoc=" + setDocument + " allowDataFill=" + allowDataFill);
newLabel = cleanControlChars(newLabel);
if (!mXMLRestoreUnderway && allowDataFill && !isStyling(LWKey.Label)) {
// If we're "styling" the label, DATA_STYLE is set -- if this is a
// DATA_STYLE, we never want to attempt a template fill -- this is a styling
// node where templates themseleves are stored.
if (isDataFormatString(newLabel)) {
final String filled = fillLabelFormat(newLabel);
setLabelFormat(newLabel);
newLabel = filled;
} else {
// clear out the saved template for dynamic data-update
setLabelFormat(null);
}
}
if (this.label == newLabel)
return;
if (this.label != null && this.label.equals(newLabel))
return;
Object old = this.label;
if (newLabel == null || newLabel.length() == 0) {
this.label = null;
if (labelBox != null)
labelBox.setText("");
} else {
this.label = newLabel;
if (DEBUG.TEXT || DEBUG.DATA) out("setLabelImpl textSet " + Util.tags(newLabel));
// todo opt: only need to do this if node or link (LWImage?)
// Handle this more completely -- shouldn't need to create
// label box at all -- why can't do entirely lazily?
if (this.labelBox == null) {
// figure out how to skip this:
//getLabelBox();
} else if (setDocument) {
try {
// note: this needs to happen before the call to layout below
getLabelBox().setText(newLabel);
} catch (Throwable t) {
Log.error(String.format("failed to set label '%s' on %s in %s", newLabel, Util.tags(getLabelBox()), this), t);
}
}
}
layout();
notify(LWKey.Label, old);
}
public void wrapLabelToWidth(final int charWidth) {
final String existingLabel = getLabel();
final String wrappedLabel = Util.formatLines(existingLabel, charWidth);
if (wrappedLabel != existingLabel) {
if (DEBUG.Enabled) Log.debug(this + " wrapped to: " + Util.tags(wrappedLabel));
setLabelImpl(wrappedLabel, true, false);
}
}
private static final int MaxLabelLineLength = VueResources.getInt("dataNode.labelLength", 50);
private String fillLabelFormat(final String fmt)
{
final String[] parts = fmt.split("\\$");
if (parts.length == 1)
return fmt;
final StringBuilder buf = new StringBuilder();
boolean anyReplacement = false;
int part = 0;
for (String s : parts) {
//Log.debug("got _part[" + s + "]");
boolean noValueFound = true;
try {
final int braceOpen = s.indexOf('{');
final int braceClose = s.indexOf('}');
if (braceOpen == 0 && braceClose > 1) {
final String keyName = s.substring(1, braceClose).trim();
//Log.debug("got __key[" + keyName + "]");
final String value = findLabelFormatDataValue(keyName);
if (value != null) {
//Log.debug("got value[" + value + "]");
// replace ${someDataKey} with the value found
buf.append(org.apache.commons.lang.StringEscapeUtils.unescapeHtml(value));
// include untouched any/all text found after the '}'
buf.append(s.substring(braceClose + 1, s.length()));
noValueFound = false;
anyReplacement = true;
}
}
} catch (Throwable t) {
Log.error("exception processing replacement braces " + Util.tags(s) + " in format " + Util.tags(fmt) + " for " + this, t);
}
if (noValueFound) {
// leave un-touched
if (part > 0)
buf.append('$'); // restore the '$' we split on
buf.append(s);
}
part++;
}
if (DEBUG.TEXT || DEBUG.DATA) {
if (anyReplacement)
Log.debug(this + " FILL made from " + Util.tags(fmt) + "->" + Util.tags(buf.toString()));
else
Log.debug(this + " FILL; NO REPLACEMENTS MADE in " + Util.tags(fmt));
}
return anyReplacement
? Util.formatLines(buf.toString(), MaxLabelLineLength)
: fmt;
}
private String findLabelFormatDataValue(String key) {
String value = getDataValue(key);
if (value == null && hasResource())
value = getResource().getProperty(key);
if (value == null) {
VueMetadataElement vme = getMetadataList().get(key);
value = (vme == null ? null : vme.getValue());
}
return tufts.vue.ds.Field.valueText(value);
}
protected tufts.vue.TextBox getLabelBox()
{
try {
if (this.labelBox == null) {
synchronized (this) {
if (this.labelBox == null)
this.labelBox = new tufts.vue.TextBox(this, this.label);
}
}
} catch (Throwable t) {
//Util.printStackTrace(t, "failed to init labelBox for " + this);
Log.error("failed to init labelBox for " + this, t);
}
return this.labelBox;
}
public void setNotes(String pNotes)
{
pNotes = cleanControlChars(pNotes);
Object old = this.notes;
if (pNotes == null) {
this.notes = null;
} else {
String trimmed = pNotes.trim();
if (trimmed.length() > 0)
this.notes = pNotes;
else
this.notes = null;
}
layout();
notify(LWKey.Notes, old);
}
/*
public void setMetaData(String metaData)
{
this.metaData = metaData;
layout();
notify("meta-data");
}
// todo: setCategory still relevant?
public void setCategory(String category)
{
this.category = category;
layout();
notify("category");
}
*/
/*
public String getCategory()
{
return this.category;
}
*/
public void takeResource(Resource resource) {
this.resource = resource;
}
public void setResource(Resource resource)
{
if (DEBUG.CASTOR) out("SETTING RESOURCE TO " + (resource==null?"":resource.getClass()) + " [" + resource + "]");
Object old = this.resource;
takeResource(resource);
layout();
if (DEBUG.CASTOR) out("NOTIFYING");
notify(LWKey.Resource, old);
/*
try {
layout();u
} catch (Exception e) {u
e.printStackTrace();
if (DEBUG.CASTOR) System.exit(-1);
}
*/
}
public Resource getResource() {
return this.resource;
}
public Resource.Factory getResourceFactory() {
final LWMap map = getMap();
if (map == null)
return Resource.getFactory();
else
return map.getResourceFactory();
}
/** convenience delegate to resource factory */
public void setResource(String spec) {
setResource(getResourceFactory().get(spec));
}
/** convenience delegate to resource factory */
public void setResource(java.net.URL url) {
setResource(getResourceFactory().get(url));
}
/** convenience delegate to resource factory */
public void setResource(java.net.URI uri) {
setResource(getResourceFactory().get(uri));
}
/** convenience delegate to resource factory */
public void setResource(java.io.File file) {
setResource(getResourceFactory().get(file));
}
// public void setResource(String urn)
// {
// if (urn == null || urn.length() == 0)
// setResource((Resource)null);
// else
// setResource(new MapResource(urn));
// }
public String getID() {
return this.ID;
}
public int getNumericID() {
return idStringToInt(getID());
}
/** for use during restore */
protected final int idStringToInt(String idStr)
{
// if (idStr != null && idStr.charAt(0) == '<') {
// // special case for internal use objects, marked with '<' as initial character
// return -1;
// }
int id = -1;
try {
id = Integer.parseInt(idStr);
} catch (Throwable t) {
Log.warn("non-numeric ID: '" + idStr + "' " + t);
// System.err.println(e + " invalid ID: '" + idStr + "'");
// e.printStackTrace();
}
return id;
}
/* public String getStyledLabel()
{
return this.label;
}*/
public String getLabel() {
return this.label;
/*
if (this.label == null)
return null;
String noHTMLString = this.label.replaceAll("\\<.*?\\>","");
noHTMLString = noHTMLString.replaceAll("\\&.*?\\;","");
noHTMLString = noHTMLString.replaceAll("\n","");
noHTMLString = noHTMLString.replaceAll("\\<!--.*?--\\>","");
noHTMLString = noHTMLString.replaceAll(" {2,}", " ").trim();
return noHTMLString;*/
}
/**
* @return a label suitable for displaying in a list: if this component
* has no label set, generate a unique name for it, and if the label has any newlines
* in it, replace them with spaces.
*/
public String getDisplayLabel() {
if (hasLabel()) {
try {
return getLabel().replace('\n', ' ');
} catch (Throwable t) {
return getUniqueComponentTypeLabel() + "[" + t + "]";
}
} else
return getUniqueComponentTypeLabel();
}
/** for debug */
public static String tag(LWComponent c) {
return c == null ? "" : c.getUniqueComponentTypeLabel();
}
public String getDiagnosticLabel() {
if (hasLabel()) {
return getUniqueComponentTypeLabel() + ": " + getLabel().replace('\n', ' ');
} else
return getUniqueComponentTypeLabel();
}
/** return a guaranteed unique name for this LWComponent */
public String getUniqueComponentTypeLabel() {
return getComponentTypeLabel() + " #" + getID();
}
/** return a type name for this LWComponent */
public String getComponentTypeLabel() {
final String name = getClass().getName();
if (name.startsWith("tufts.vue.LW"))
return name.substring(12);
else if (name.startsWith("tufts.vue."))
return name.substring(10);
else
return name;
}
String toName() {
if (getLabel() == null)
return getDisplayLabel();
else
return getComponentTypeLabel() + "[" + getLabel() + "]";
}
/** @deprecated
* left in for (possible future) backward file compatibility
* do nothing with this data anymore for now.
**/
public synchronized NodeFilter getNodeFilter() {
// if the double-checked locking idiom was reliable in java, we'd use it here, but
// since it's not, we synchronize this whole method.
if (nodeFilter == NEEDS_NODE_FILTER) {
//Util.printStackTrace("lazy create of node filter for " + this);
nodeFilter = new NodeFilter();
}
return nodeFilter;
}
/** @deprecated -- for persistance */
public void setXMLnodeFilter(NodeFilter nodeFilter) {
this.nodeFilter = nodeFilter;
}
/** @deprecated -- return null if the node filter is empty, so we don't bother with entry in the save file */
public NodeFilter getXMLnodeFilter() {
if (mXMLRestoreUnderway) {
// in case validation is on:
return nodeFilter;
} else if (nodeFilter == NEEDS_NODE_FILTER || (nodeFilter != null && nodeFilter.size() < 1)) {
return null;
} else
return nodeFilter;
}
/** does this support a user editable label? */
// TODO: resolve this with supportsProperty(LWKey.Label) (perhaps lose this method)
public boolean supportsUserLabel() {
return supportsProperty(LWKey.Label);
}
/** does this support user resizing? */
// TODO: change these "supports" calls to an arbitrary property list
// that could have arbitrary properties added to it by plugged-in non-standard tools
public boolean supportsUserResize() { return false; }
/** @return false: subclasses (e.g. containers), override to return true if allows children dragged in and out
* by a user.
*/
public boolean supportsChildren() { return false; }
/** @Return true: subclasses (e.g. containers), override to return false if you never want this component
reparented by users */
// todo: handle via API that LWGroup can declare
public boolean supportsReparenting() { return parent instanceof LWGroup == false; }
/** @return true: by default, all objects can be selected with other objects at the same time */
public boolean supportsMultiSelection() { return true; }
/** @return false by default -- only containers can have slides */
public boolean supportsSlide() { return false; }
/** @return false by default -- override to initiate dupe and system drag */
public boolean supportsCopyOnDrag() { return false; }
/** @return true if we allow a link to the target, and the target allows a link to us.
* Eventually we can use this to check ontology information.
* @param target -- the target to check. If null, tells is if this component allows
* link to nothing / allows links at all.
*/
public boolean canLinkTo(LWComponent target) {
return canLinkToImpl(target) && (target == null || target.canLinkToImpl(this));
}
/** @return true -- subclass impl's can override */
protected boolean canLinkToImpl(LWComponent target) {
return hasFlag(Flag.LOCKED) == false;
}
public boolean hasLabel() {
return this.label != null && this.label.length() > 0;
}
public String getNotes() {
return this.notes;
}
public boolean hasNotes() {
return this.notes != null && this.notes.length() > 0;
}
public boolean hasResource() {
return this.resource != null;
}
public boolean hasLinks() {
return mLinks != null && mLinks.size() > 0;
}
/*
public String getMetaData()
{
return this.metaData;
}
public boolean hasMetaData()
{
return this.metaData != null;gajendracircle
}
*/
public boolean inPathway()
{
return mPathways != null && mPathways.size() > 0;
}
public boolean inVisiblePathway()
{
if (inPathway())
for (LWPathway p : mPathways)
if (p.isDrawn())
return true;
return false;
}
/** Is component in the given pathway? */
// rename onPathway?
public boolean inPathway(LWPathway path)
{
if (mPathways == null || path == null)
return false;
for (LWPathway p : mPathways)
if (p == path)
return true;
return false;
}
/** @return null if we're in more than one visible pathway, or the LWPathway we're on if it's the only visible one */
public LWPathway getExclusiveVisiblePathway()
{
if (mPathways == null)
return null;
boolean foundOne = false;
LWPathway singleVisible = null;
for (LWPathway p : mPathways) {
if (p.isDrawn()) {
if (foundOne)
return null;
foundOne = true;
singleVisible = p;
}
}
return singleVisible;
}
public List<LWPathway> getPathways() {
return mPathways == null ? java.util.Collections.EMPTY_LIST : mPathways;
}
/**
* @return true if this component is in a pathway that is
* drawn with decorations (e.g., not a reveal-way)
*/
public boolean inDrawnPathway()
{
if (mPathways == null)
return false;
for (LWPathway p : mPathways)
if (p.isVisible() && !p.isRevealer())
return true;
return false;
}
public boolean hasEntries() {
return mEntries != null && mEntries.size() > 0;
}
public int numEntries() {
return mEntries == null ? 0 : mEntries.size();
}
public List<LWPathway.Entry> getEntries() {
return mEntries;
}
protected void addEntryRef(LWPathway.Entry e) {
if (mEntries == null) {
mEntries = new ArrayList();
mVisibleSlideIconIterator = new SlideIconIter();
}
if (!mEntries.contains(e))
mEntries.add(e);
addPathwayRef(e.pathway);
}
protected void removeEntryRef(LWPathway.Entry e) {
if (mEntries == null) {
Util.printStackTrace(this + "; no entries! can't remove: " + e);
return;
}
if (!mEntries.remove(e))
Util.printStackTrace(this + "; Warning: didn't contain entry " + e);
removePathwayRef(e.pathway);
}
private void addPathwayRef(LWPathway p)
{
if (mPathways == null)
mPathways = new ArrayList();
if (!mPathways.contains(p)) {
mPathways.add(p);
// todo: too late, UNDELETING flag already cleared (call is from pathway on pathway undelete)
// okay for now: re-layout on undo should be harmless, but generates lots
// of needless location events that clog up event debugging when doing undo's
if (!hasFlag(Flag.UNDELETING) && LWIcon.IconPref.getPathwayIconValue())
layout();
}
//notify("pathway.add");
}
private void removePathwayRef(LWPathway p)
{
if (mPathways == null) {
if (DEBUG.META) tufts.Util.printStackTrace("attempt to remove non-existent pathwayRef to " + p + " in " + this);
return;
}
mPathways.remove(p);
// clear any hidden bits that may be set as a result
// of the membership in the pathway.
for (HideCause cause : HideCause.values())
if (cause.type == CAUSE_PATHWAY)
clearHidden(cause);
if (!hasFlag(Flag.DELETING) && LWIcon.IconPref.getPathwayIconValue()) {
// todo: handle at higher level or have icon block listen for some event
layout();
}
//notify("pathway.remove");
}
/** @deprecated - not really deprecated, but intended for persistance only */
public java.awt.Dimension getXMLtextBox() {
return null;
// NOT CURRENTLY USED
/*
if (this.labelBox == null)
return null;
else
return this.labelBox.getSize();
*/
}
/** @deprecated - not really deprecated, intended for persistance only */
public void setXMLtextBox(java.awt.Dimension d) {
//this.textSize = d;
}
/** for persistance */
// todo: move all this XML handling stuff to a special castor property mapper,
// presumably in conjunction with re-architecting the whole mapping style &
// save mechanism.
public String getXMLlabel()
{
return this.label;
//return tufts.Util.encodeUTF(this.label);
}
/** for persistance */
public void setXMLlabel(String text)
{
setLabel(unEscapeNewlines(text));
//this.label = unEscapeNewlines(text);
//getLabelBox().setText(this.label);
// we want to make sure layout() is not called,
// and currently there's no need to do notify's during init.
}
/** for persistance */
public String getXMLnotes()
{
//return this.notes;
// TODO: can escape newlines new with
and tab with
return escapeWhitespace(this.notes);
}
/** for persistance -- gets called by castor after it reads in XML */
public void setXMLnotes(String text)
{
setNotes(decodeCastorMultiLineText(text));
}
protected static String decodeCastorMultiLineText(String text)
{
// If castor xml indent was on when save was done
// (org.exolab.castor.indent=true in castor.properties
// somewhere in the classpath, to make the XML more human
// readable) it will break up elements like: <note>many chars
// of text...</note> with newlines and whitespaces to indent
// the new lines in the XML -- however, on reading them back
// in, it puts this white space into the string you saved! So
// when we save we're sure to manually encode newlines and
// runs of white space, so when we get here, if see any actual
// newlines followed by runs of white space, we know to trash
// them because it was castor formatting fluff. (btw, this
// isn't a problem for labels because they're XML attributes,
// not elements, which are quoted).
// Update: As of castor 0.9.7, this no longer appears true
// (it doesn't indent new text lines with white space
// even after wrapping them), but we still need this
// here to deal with old save files.
text = text.replaceAll("\n[ \t]*%nl;", "%nl;");
text = text.replaceAll("\n[ \t]*", " ");
return unEscapeWhitespace(text);
}
// FYI, this is no longer needed for castor XML attributes, as
// of version 0.9.7 it automatically encodes & preserves them.
// Note that this is still NOT true for XML elements.
private static String escapeNewlines(String text)
{
if (text == null)
return null;
else {
return text.replaceAll("[\n\r]", "%nl;");
}
}
private static String unEscapeNewlines(String text)
{
if (text == null)
return null;
else {
return text.replaceAll("%nl;", "\n");
}
}
public static String escapeWhitespace(String text)
{
if (text == null)
return null;
else {
text = text.replaceAll("%", "%pct;");
// replace all instances of two spaces with space+%sp;
// to break them up (and thus we wont lose space runs)
text = text.replaceAll(" ", " %sp;");
text = text.replaceAll("\t", "%tab;");
return escapeNewlines(text);
}
}
public static String unEscapeWhitespace(String text)
{
if (text == null)
return null;
else {
text = unEscapeNewlines(text);
text = text.replaceAll("%tab;", "\t");
text = text.replaceAll("%sp;", " ");
return text.replaceAll("%pct;", "%");
}
}
private static final Object LAYOUT_DEFAULT = "default";
/** Layout this component and all children, if any. Normally this would only be called on an
* LWMap, but in some cases, any component might effectively be "at the top level" while in an
* intermediate state, such as components in a cut-buffer before they've been pasted out to a
* map. */
public void layoutAll(Object triggerKey) {
layout(triggerKey);
}
/**
* If this component supports special layout for it's children,
* or resizes based on font, label, etc, do it here.
*/
public final void layout() {
if (mXMLRestoreUnderway == false)
layout(LAYOUT_DEFAULT);
}
final void layout(Object triggerKey) {
if (mXMLRestoreUnderway == false) {
layoutImpl(triggerKey);
if (triggerKey == LWMap.NODE_INIT_LAYOUT) {
validateInitialValues();
layoutSlideIcons(null);
} else if (triggerKey == LWMap.LINK_INIT_LAYOUT) {
validateInitialValues();
}
// need a reshape/reshapeImpl for this (size/location changes)
//if (mSlideIconBounds != null)
// mSlideIconBounds.x = Float.NaN; // invalidate
}
updateConnectedLinks(null);
}
protected boolean validateInitialValues() {
boolean bad = false;
// Note that if ANY component in the map has a NaN coordinate or dimension,
// it can put the AWT graphics routines into an unrecoverable state that
// may prevent the entire map from drawing sanely or at all.
if (Float.isNaN(x)) {
Log.warn("bad x " + this);
x = 0;
bad = true;
}
if (Float.isNaN(y)) {
Log.warn("bad y " + this);
y = 0;
bad = true;
}
if (Float.isNaN(width)) {
Log.warn("bad width " + this);
width = 0;
bad = true;
}
if (Float.isNaN(height)) {
Log.warn("bad height " + this);
height = 0;
bad = true;
}
if (supportsProperty(KEY_FontSize) && mFontSize.get() < 1) {
Log.warn("bad font size " + mFontSize.get() + " " + this);
mFontSize.take(1); // don't risk triggering an event at a bad time
bad = true;
}
return bad;
}
protected void layoutImpl(Object triggerKey) {}
/** @return true: default is always autoSized */
//public boolean isAutoSized() { return true; }
public boolean isAutoSized() { return false; } // LAYOUT-NEW
/** do nothing: default is always autoSized */
public void setAutoSized(boolean t) {}
public void setToNaturalSize() {
setAutoSized(false);
}
private static boolean eq(Object a, Object b) {
return a == b || (a != null && a.equals(b));
}
public boolean isTransparent() {
return mFillColor.isTransparent();
}
public boolean isTranslucent() {
return mFillColor.isTranslucent();
}
/**
* Color to use at draw time. LWNode overrides to provide darkening of children.
* We also use this for the background color in active on-map text edits.
*/
public Color getRenderFillColor(DrawContext dc) {
if (mFillColor.isTransparent()) {
if (dc != null && dc.focal == this) {
//System.out.println(" DC FILL: " + dc.getFill() + " " + this);
return dc.getBackgroundFill();
} else if (parent != null) {
//System.out.println(" PARENT FILL: " + parent.getRenderFillColor(dc) + " " + this);
return parent.getRenderFillColor(dc);
}
}
//System.out.println("DEFAULT FILL: " + mFillColor.get() + " " + this);
return mFillColor.get();
}
public Color getFinalFillColor(DrawContext dc) {
if (mFillColor.isTransparent()) {
Color c = null;
if (getParent() != null)
return getParent().getFinalFillColor(dc);
else if (dc != null)
return dc.getBackgroundFill();
else
return null;
} else
return getFillColor();
}
public static Color getContrastColor(Color c) {
if (c != null) {
if (c.equals(Color.black))
return Color.darkGray;
else
return c.darker();
} else {
return DEBUG.BOXES ? Color.red : Color.gray;
}
}
public Color getContrastStrokeColor(DrawContext dc) {
final Color renderFill = getRenderFillColor(dc);
if (renderFill != null && !isTransparent()) {
return getContrastColor(renderFill);
} else {
// transparent fill: just use stroke color
return getStrokeColor();
// transparent fill: base on stroke color
//return getStrokeColor().brighter();
}
}
//private LWPathway lastPriorit;
public Color getPriorityPathwayColor(DrawContext dc) {
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;
//return getRenderFillColor(dc);
}
void takeFillColor(Color color) {
mFillColor.take(color);
}
// We still need these standard style setters & getters for backward compat
// with all sorts of old code, and espcially for persistance (the castor
// mapping, which refers to these methods)
public float getStrokeWidth() { return mStrokeWidth.get(); }
public void setStrokeWidth(float w) { mStrokeWidth.set(w); }
/** @return null for SOLID (ordinal 0, the default, as for old save files), or otherwise, the ordinal of the style enum
* Castor will not bother to generate the attribute/element when it's value is null. */
public Integer getXMLstrokeStyle() {
int code = mStrokeStyle.get().ordinal();
return code == 0 ? null : code;
}
public void setXMLstrokeStyle(Integer ordinal) {
// todo: have the Key class process enum's generically, caching the results of Class<? extends Enum>.getEnumConstants()
for (StrokeStyle ss : StrokeStyle.values()) {
if (ss.ordinal() == ordinal) {
mStrokeStyle.set(ss);
break;
}
}
}
public Color getFillColor() { return mFillColor.get(); }
public void setFillColor(Color c) { mFillColor.set(c); }
public String getXMLfillColor() { return mFillColor.asString(); }
public void setXMLfillColor(String xml) { mFillColor.setFromString(xml); }
public Color getTextColor() { return mTextColor.get(); }
public void setTextColor(Color c) { mTextColor.set(c); }
public String getXMLtextColor() { return mTextColor.asString(); }
public void setXMLtextColor(String xml) { mTextColor.setFromString(xml); }
public Color getStrokeColor() { return mStrokeColor.get(); }
public void setStrokeColor(Color c) { mStrokeColor.set(c); }
public String getXMLstrokeColor() { return mStrokeColor.asString(); }
public void setXMLstrokeColor(String xml) { mStrokeColor.setFromString(xml); }
public Font getFont() { return mFont.get(); }
public void setFont(Font font) { mFont.set(font); }
public String getXMLfont() { return mFont.asString(); }
public void setXMLfont(String xml) { mFont.setFromString(xml); }
/**
* The first time a TextBox is created for edit, it may not have been laid out
* by it's parent, which is where it normally gets it's location. This
* initializes the location of the TextBox for first usage. The default
* impl here centers the TextBox in the LWComponent.
*/
public void initTextBoxLocation(TextBox textBox) {
textBox.setBoxCenter(getWidth() / 2,
getHeight() / 2);
}
public final LWContainer getParent() {
return this.parent;
}
/** @return what layer we're inside */
public LWMap.Layer getLayer() {
if (parent == null)
return null;
else if (parent instanceof LWMap.Layer)
return (LWMap.Layer) parent;
else
return parent.getLayer();
}
/**
* for castor persistance
* @return null if we are not a child of a layer, or the layer if it's our immediate parent */
public LWMap.Layer getPersistLayer() {
if (parent instanceof LWMap.Layer)
return (LWMap.Layer) parent;
else
return null;
}
/** for castor persistance */
public void setPersistLayer(LWMap.Layer layer) {
//if (!VUE.VUE3_LAYERS) return;
setParent(layer);
}
public int getDepth() {
if (parent == null)
return 0;
else
return parent.getDepth() + 1;
}
public int getIndex() {
if (parent == null)
return -1;
else
return parent.indexOf(this);
}
protected void setParent(LWContainer newParent)
{
if (DEBUG.UNDO) System.err.println("*** SET-PARENT: " + newParent + " for " + this);
//final boolean linkNotify = (!mXMLRestoreUnderway && parent != null);
if (parent == newParent) {
// This is normal.
// (e.g., one case: during undo of reparenting operations)
//if (DEBUG.Enabled) Util.printStackTrace("redundant set-parent in " + this + "; parent=" + newParent);
return;
}
if (newParent.hasAncestor(this)) {
Util.printStackTrace("ATTEMPTED PARENT LOOP " + this + " can't make a child our parent: " + newParent);
return;
}
//-----------------------------------------------------------------------------
// We want to make sure schema references are current every time a node is put
// into to the user space (may be seen or edited by a user). This includes new
// nodes to the user space, as well as pre-existing nodes being returned to the
// user space (from an undo queue or cut buffer).
//
// So this handles updating the schema reference during restore, during undo,
// and for paste operations of nodes that may have been in the cut/copy buffer.
// The reason we need to do this is that a data source may have been loaded
// while this nodes were out of user space, creating a new live schema that one
// of these nodes may now want to be pointing to.
//
// TODO: at the moment, this is a bit overkill, as it will also be run every
// time a node is reparented at all, tho the operation is idempotent so we can
// live with it. A combination of calling this in restoreToModel (for undo),
// and only calling this here if parent was previously null (persistance,
// cut/paste), should handle that.
if (!mXMLRestoreUnderway) {
// handled specially during restored
validateSchemaReference();
}
//-----------------------------------------------------------------------------
parent = newParent;
//layout(); // for preference change updates
// if (linkNotify && mLinks.size() > 0)
// for (LWLink link : mLinks)
// link.notifyEndpointReparented(this);
}
//protected void reparentNotify(LWContainer parent) {}
/**
* for now (2007-11-30) just records sync source in case we want to use it later,
* but does not set up data synchronization (as per Melanie 2007-11-14)
*/
public void setSyncSource(LWComponent source) {
mSyncSource = source;
if (true) return; // all dynamic data syncing disabled for now as per Melanie -- SMF 2007-11-14
if (mSyncClients != null) {
out("blowing away sync clients on syncSource set");
// just in case
mSyncClients.clear();
mSyncClients = null;
}
mSyncSource.addSyncClient(this);
}
public LWComponent getSyncSource() {
return mSyncSource;
}
protected void addSyncClient(LWComponent c) {
if (mSyncClients == null)
mSyncClients = new HashSet();
mSyncClients.add(c);
}
/** set the given component as the style for this object, applying it's style properties to us
* Note: this will force the STYLE bit to be set on parentStyle if it already isn't set
*/
public void setStyle(LWComponent parentStyle)
{
if (DEBUG.STYLE) out("setStyle " + parentStyle);
takeStyle(parentStyle);
if (parentStyle != null) {
parentStyle.setFlag(Flag.STYLE);
if (!mXMLRestoreUnderway) // we can skip the copy during restore
copyStyle(parentStyle);
}
}
void takeStyle(LWComponent parentStyle) {
mParentStyle = parentStyle;
}
/** for castor persist */
public LWComponent getStyle() {
return mParentStyle;
}
public boolean isStyle() {
return hasFlag(Flag.STYLE);
//return isStyle;
}
/** @return Boolean.TRUE if this component is serving as an active style for other objects, null otherwise */
public Boolean getPersistIsStyle() {
return isStyle() ? Boolean.TRUE : null;
}
public void setPersistIsStyle(Boolean b) {
setFlag(Flag.STYLE, b.booleanValue());
}
/** @return Boolean.TRUE if this component has marked as having the special "slide" style, null otherwise */
public Boolean getPersistIsSlideStyled() {
return hasFlag(Flag.SLIDE_STYLE) ? Boolean.TRUE : null;
}
public void setPersistIsSlideStyled(Boolean b) {
setFlag(Flag.SLIDE_STYLE, b.booleanValue());
}
/** @deprecated: tmp back compat only */ public void setParentStyle(LWComponent c) { setStyle(c); }
/** @deprecated: tmp back compat only */ public Boolean getPersistIsStyleParent() { return null; }
/** @deprecated: tmp back compat only */ public void setPersistIsStyleParent(Boolean b) { setPersistIsStyle(b); }
/** @deprecated: tmp back compat only */ public LWComponent getParentStyle() { return null; }
/**
* @return 0 by default
* the pick depth (in PickContext) must be >= what this returns for descdents of this component
* be picked (selected, etc). Mostly meaningful when an LWContainer subclass implements
* and returns something > 0, tho a single component could use this to become a "background" item.
* You can think of this as establishing a "wall" in the depth hierarchy, past which pick
* traversals will not descend unless given a high enough pickDepth to jump the wall.
*/
public int getPickLevel() {
return 0;
}
//private static LWComponent ProxySlideComponent = new LWComponent("<global-slide-proxy>");
/** return the component to be picked if we're picked: e.g., may return null if you only want children picked, and not the parent */
protected LWComponent defaultPick(PickContext pc) {
// If we're dropping something, never allow us to be picked
// if we're a descendent of what's being dropped! (would be a parent/child loop)
if (pc.dropping != null && pc.dropping instanceof LWContainer && hasAncestor((LWComponent)pc.dropping))
return null;
// else if (isDrawingSlideIcon() && getMapSlideIconBounds().contains(pc.x, pc.y)) {
// return getEntryToDisplay().getSlide();
// }
else
return defaultPickImpl(pc);
}
protected LWComponent defaultPickImpl(PickContext pc) {
return this;
}
/** If PickContext.dropping is a LWComponent, return parent (as we can't take children),
* otherwise return self
*/
protected LWComponent defaultDropTarget(PickContext pc) {
// TODO: if this is a system drag, dropping is null,
// and we don't know if this is a localDrop of a node,
// or a drop of a resource, so, for example, links
// will incorrectly get targeted for local node system drops.
// (tho when dropped, it'll still just get added to the parent).
if (pc.dropping instanceof LWComponent)
return getParent();
else
return this;
}
public boolean isOrphan() {
return this.parent == null;
}
public boolean atTopLevel() {
return parent != null && parent.isTopLevel();
}
public boolean isTopLevel() {
return false;
}
public boolean hasChildren() {
return false;
}
/** @return false; overriding impl's should return true if this
* component has children, and those children are always fully contained
* within the bounds of the parent */
public boolean fullyContainsChildren() {
return false;
}
public boolean hasChild(LWComponent c) {
return false;
}
public boolean isLaidOut() {
return isManagedLocation();
}
public boolean isManagedLocation() {
return (parent != null && parent.isManagingChildLocations()) || (isSelected() && isAncestorSelected());
}
public boolean isManagingChildLocations() {
return false;
}
/** @return true - A single component always "has content" -- subclasses override to provide varying semantics */
public boolean hasContent() {
return true;
}
/** @return false by default */
public boolean isTextNode() {
return false;
}
/** @return false by default */
public boolean isLikelyTextNode() {
return false;
}
/** @return false by default */
public boolean isExternalResourceLinkForPresentations() {
return false;
}
public final void addChild(LWComponent c) {
addChildren(Collections.singletonList(c), ADD_DEFAULT);
}
public final void dropChild(LWComponent c) {
addChildren(Collections.singletonList(c), ADD_DROP);
}
public final void pasteChild(LWComponent c) {
addChildren(Collections.singletonList(c), ADD_PASTE);
}
public final void addChildren(List<? extends LWComponent> children) {
addChildren(children, ADD_DEFAULT);
}
/**
* Although unsupported on LWComponents (must be an LWContainer subclass to support children),
* this method appears here for typing convenience and debug. If a non LWContainer subclass
* calls this, it's a no-op, and a diagnostic stack trace is dumped to the console.
*/
public void addChildren(Collection<? extends LWComponent> children, Object context) {
Util.printStackTrace(this + ": can't take children; ignored: " + Util.tags(children) + "; context=" + context);
}
/** return true if this component is only a "virtual" member of the map:
* It may report that it's parent is in the map, but that parent doesn't
* list the component as a child (so it will never be drawn or traversed
* when handling the entire map).
*/
public boolean isMapVirtual() {
return getParent() == null || !getParent().hasChild(this);
}
/** @return 0 -- override to support children */
public int numChildren() {
return 0;
}
/** @deprecated - use getChildren */
public java.util.List<LWComponent> getChildList()
{
return NO_CHILDREN;
}
public java.util.List<LWComponent> getChildren()
{
return NO_CHILDREN;
}
/** @return 0 -- override for container impls */
public int getDescendentCount() {
return 0;
}
/** @return: always null */
public LWComponent getChild(int index) {
return null;
}
public boolean hasPicks() {
return (hasChildren() && !isCollapsed()) || hasEntries();
}
/** ordered for drawing and picking */
private final class SlideIconIter implements Iterator<LWSlide>, Iterable<LWSlide>
{
int nextIndex;
LWSlide nextSlide;
LWSlide onTop;
DrawContext dc;
LWPathway activePathway;
LWPathway.Entry activeEntry;
private SlideIconIter() {
//System.out.println("\nSlideIter, entries=" + mEntries.size());
advance();
}
private void advance() {
//out("advance; nextIndex =" + nextIndex);
nextSlide = null;
int i = nextIndex;
for (; i < mEntries.size(); i++) {
final LWPathway.Entry e = mEntries.get(i);
//out("inspecting index " + i + " " + e);
if (e.hasVisibleSlide()) {
final LWSlide slide = e.getSlide();
if (activePathway == null && slide.isSelected()) {
onTop = slide;
//} else if (slide.getEntry().pathway == activePathway) {
} else if (slide.getPathwayEntry() == activeEntry) {
onTop = slide;
} else {
nextSlide = slide;
break;
}
}
}
nextIndex = i + 1;
// if we're at the end, provide the selected (if there was one)
if (nextSlide == null) {
nextSlide = onTop;
onTop = null;
}
}
public boolean hasNext() {
final boolean t = (nextSlide != null);
//out("hasNext " + t);
if (nextIndex > 100) {
Util.printStackTrace("loop");
return false;
}
return t;
//return nextSlide != null;
}
public LWSlide next() {
if (nextSlide == null) {
if (DEBUG.Enabled) Util.printStackTrace(this + "; next at end of SlideIconIter; entries=" + mEntries);
return null;
}
final LWSlide s = nextSlide;
advance();
//out("return " + s);
return s;
}
public void remove() { throw new UnsupportedOperationException(); }
public Iterator<LWSlide> iterator() {
// reset when re-used
nextIndex = 0;
nextSlide = null;
onTop = null;
if (dc != null && dc.isPresenting()) {
activePathway = VUE.getActivePathway();
//activeEntry = VUE.getActiveEntry();
} else {
activePathway = null;
//activeEntry = null;
}
activeEntry = VUE.getActiveEntry();
advance();
return this;
}
}
/** @return the slides for drawing as slide icons in the current picking and drawing order */
private final Iterable<LWSlide> seenSlideIcons(DrawContext dc) {
// if (mEntries == null || mEntries.size() == 0) {
// // this is sort of overkill, as we shouldn't even be calling this if hasEntries is false
// return Util.EmptyIterable;
// } else
if (mEntries.size() == 1) {
final LWPathway.Entry e = mEntries.get(0);
if (e.hasVisibleSlide())
return Util.iterable(e.getSlide());
else
return Util.EmptyIterable;
} else {
mVisibleSlideIconIterator.dc = dc;
return mVisibleSlideIconIterator;
}
//return new SlideIter();
}
/**
* @return a list, to be traversed in reverse order. If a new list needs to be constructed,
* it will dumped into stored, which will be returned. Otherwise, an internal list may be returned.
*/
public List<LWComponent> getPickList(PickContext pc, List<LWComponent> stored)
{
Iterable<LWSlide> seenSlides = null;
if (pc.root != this && hasEntries() && (seenSlides = seenSlideIcons(pc.dc)) != Util.EmptyIterable) {
// todo performance: see LWTraversal for comments: change impl to return a ReverseListIterator, etc.a
stored.clear();
if (!isCollapsed())
stored.addAll(getChildren());
for (LWSlide s : seenSlides)
stored.add(s);
return stored;
} else
return (List) getChildren();
}
public java.util.Iterator<? extends LWComponent> getChildIterator() {
return EmptyIterator;
}
/** The default is to get all ChildKind.PROPER children (backward compatability)
* This impl always returns an empty list. Subclasses that can have proper
* children provide the impl for that
*/
public Collection<LWComponent> getAllDescendents() {
// Default is only CHILD_PROPER, and by definition,
// LWComponents have no proper children.
// return getAllDescendents(CHILD_PROPER);
return java.util.Collections.EMPTY_LIST;
}
public Collection<LWComponent> getAllDescendents(final ChildKind kind) {
if (kind == ChildKind.PROPER)
return java.util.Collections.EMPTY_LIST;
else
return getAllDescendents(kind, new java.util.ArrayList(), Order.TREE);
}
public final Collection<LWComponent> getAllDescendents(final ChildKind kind, final Collection<LWComponent> bag) {
return getAllDescendents(kind, bag, Order.TREE);
}
/** @return bag -- a noop -- this is meant to be overriden */
public Collection<LWComponent> getAllDescendents(final ChildKind kind, final Collection<LWComponent> bag, Order order) {
return bag;
}
/** @return getDescendentsOfType(ChildKind.PROPER, clazz) */
public <A extends LWComponent> Iterable<A> getDescendentsOfType(Class<A> clazz) {
return getDescendentsOfType(ChildKind.PROPER, clazz);
}
/** @see LWContainer (this impl EmptyIterator) */
public <A extends LWComponent> Iterable<A> getDescendentsOfType(ChildKind kind, Class<A> clazz) { return EmptyIterable; }
/** @see LWContainer (this impl EmptyIterator) */
public <A extends LWComponent> Iterable<A> getChildrenOfType(Class<A> clazz) { return EmptyIterable; }
/** @see LWContainer (this impl EmptyIterator) */
public Iterator<LWNode> getAllNodesIterator() { return EmptyIterator; }
/** @see LWContainer (this impl EmptyIterator) */
public Iterator<LWLink> getAllLinksIterator() { return EmptyIterator; }
/** @see LWContainer (this impl EmptyIterator) */
public Iterator<LWNode> getChildNodeIterator() { return EmptyIterator; }
/** @see LWContainer (this impl EmptyIterator) */
public Iterator<LWLink> getChildLinkIterator() { return EmptyIterator; }
/** for tracking who's linked to us */
void addLinkRef(LWLink link)
{
if (DEBUG.UNDO) out(this + " adding link ref to " + link);
if (mLinks == null)
mLinks = new ArrayList(4);
if (mLinks.contains(link)) {
//tufts.Util.printStackTrace("addLinkRef: " + this + " already contains " + link);
if (DEBUG.Enabled) Log.warn("addLinkRef: " + this + " already contains " + link);
} else {
mLinks.add(link);
notify(LWKey.LinkAdded, link); // informational only event
}
}
/** for tracking who's linked to us */
void removeLinkRef(LWLink link)
{
if (DEBUG.EVENTS||DEBUG.UNDO) out("removeLinkRef: " + link);
if (mLinks == null || !mLinks.remove(link))
Log.warn("removeLinkRef: " + this + " didn't contain " + link);
clearHidden(HideCause.PRUNE); // todo: ONLY clear this if we were pruned by the given link!
notify(LWKey.LinkRemoved, link); // informational only event
}
/** @return us all the links who have us as one of their endpoints */
public List<LWLink> getLinks(){
return mLinks == null ? Collections.EMPTY_LIST : mLinks;
}
public List<LWLink> getIncomingLinks(){
// Note: it's inefficient to call getIncomingLinks() AND getOutgoingLinks() because bi-directional
// links will be returned by both and you'll double-process them. If you want both incoming and
// outgoing links just call getLinks() and there won't be duplicates.
List<LWLink> incomingLinks = new ArrayList<LWLink>();
for (LWLink link : getLinks()) {
int arrowState = link.getArrowState();
boolean arrowHead = arrowState == LWLink.ARROW_HEAD,
arrowTail = arrowState == LWLink.ARROW_TAIL,
arrowNonDirectional = arrowState == LWLink.ARROW_BOTH || arrowState == LWLink.ARROW_NONE;
if (this == link.getHead()) {
if (arrowHead || arrowNonDirectional) {
incomingLinks.add(link);
}
} else if (this == link.getTail()) {
if (arrowTail || arrowNonDirectional) {
incomingLinks.add(link);
}
}
}
return incomingLinks;
}
public List<LWLink> getOutgoingLinks(){
// Note: it's inefficient to call getIncomingLinks() AND getOutgoingLinks() because bi-directional
// links will be returned by both and you'll double-process them. If you want both incoming and
// outgoing links just call getLinks() and there won't be duplicates.
List<LWLink> outgoingLinks = new ArrayList<LWLink>();
for (LWLink link : getLinks()) {
int arrowState = link.getArrowState();
boolean arrowHead = arrowState == LWLink.ARROW_HEAD,
arrowTail = arrowState == LWLink.ARROW_TAIL,
arrowNonDirectional = arrowState == LWLink.ARROW_BOTH || arrowState == LWLink.ARROW_NONE;
if (this == link.getHead()) {
if (arrowTail || arrowNonDirectional) {
outgoingLinks.add(link);
}
} else if (this == link.getTail()) {
if (arrowHead || arrowNonDirectional) {
outgoingLinks.add(link);
}
}
}
return outgoingLinks;
}
// /** get all links to us + to any descendents */
// public List getAllLinks() {
// return getLinks();
// }
/** @return all components at the far end of any links that are connected to us
* Note that if this is called on an LWLink, it will only return objects linking to us,
* not the objects at our endpoints.
**/
public Collection<LWComponent> getLinked() {
// default uses a set, in case there are multiple links to the same endpoint
return getLinked(new HashSet(getLinks().size()));
}
protected Collection<LWComponent> getLinked(Collection<LWComponent> bag) {
return getLinked(getLinks(), bag);
}
/** given the set of links, which should be sub-set of our own links, return a set containting the far endpoints */
public Collection<LWComponent> getLinked(Collection<LWLink> links, Collection<LWComponent> bag)
{
for (LWLink link : links) {
final LWComponent head = link.getHead();
final LWComponent tail = link.getTail();
if (head == tail)
; // ignore circular links
else if (head == this && tail != null)
bag.add(tail);
else if (tail == this && head != null)
bag.add(head);
}
return bag;
}
/** @return all nodes that are *probable* clustering targets for this node. Currently,
* this usually means most or all of the nodes this node is linked to */
public Collection<LWComponent> getClustered()
{
final List<LWLink> links = getLinks();
int dcl = 0;
for (LWLink l : links)
if (l.isDataCountLink())
dcl++;
if (dcl == 0 || dcl == links.size())
return getLinked(links, new HashSet(links.size()));
else
return getLinked(getPriorityDataLinks(getLinks()), new HashSet(dcl));
}
/** @return list will only contain LWLink */
private static Collection<LWLink> getPriorityDataLinks(Collection<LWLink> links)
{
// What we really need here is a way to tell the significance of the
// count-link itself. E.g., if we find a count-link to an endpoint that has
// NO OTHER count-links to it all, that's an easy case -- we want to cluster
// that item near us. But if that endpoint has any OTHER count links,
// either we do NOT want it clustering near us, or we might want to go so
// far as to find the count link with the highest count and use that, etc.
// In any case, that's all getting quite complicated to add now. We'd have
// to fetch all the endpoints and inspect them in conjunction with the
// links.
// For now, we just ignore count-links entirely, even tho they
// produce FANTASTIC clustering in certian special cases.
// (e.g., all the countries clustering around a region they're in)
//final Collection<LWLink> countDataLinks = new ArrayList();
final Collection<LWLink> normalDataLinks = new ArrayList(links.size());
// todo performance: just produce integer counts, then construct the
// lists in the rare case they're needed
for (LWLink l : links) {
if (l.isDataCountLink())
;//countDataLinks.add(l);
else
normalDataLinks.add(l);
}
return normalDataLinks;
// if (countDataLinks.size() == 1 && otherDataLinks.size() > 0) {
// return otherDataLinks;
// } else {
// return links;
// }
}
/** @return all components directly connected to this one: for most components, this
* is just all the LWLink's that connect to us. For LWLinks, it's mainly it's endpoints,rg
* plus also any LWLink that may be directly connected to the link itself
*/
public Collection<? extends LWComponent> getConnected() {
return Collections.unmodifiableList(getLinks());
}
/** @return a list of every component connected to this one via links, including the links themselves */
public Collection<LWComponent> getLinkChain() {
return getLinkChain(new HashSet(), null);
}
/**
* @return a list of every component connected to this one via links, including the links themselves
* @param bag - the collection to store the results in. Any component already in the bag will not
* have it's outbound links followed -- this provides inherent loop protection.
* Note that if this collection isn't a Set of some kind, components will appear in the bag more than once.
* (Once for every time they were visited).
*/
public final Collection<LWComponent> getLinkChain(Collection bag, LWComponent backstop)
{
if (DEBUG.LINK) Log.debug("getLinkChain: " + this);
return getLinkChainImpl(bag, backstop, 0);
}
private static void tabout(int depth, String s) {
for (int x = 0; x < depth; x++) System.out.print(" ");
System.out.println(s);
}
private Collection<LWComponent> getLinkChainImpl(Collection bag, LWComponent backstop, int depth)
{
if (!bag.add(this)) {
// already added to the set with all connections -- don't process again
if (DEBUG.LINK) tabout(depth, " (dupe)" + this);
return bag;
}
if (DEBUG.LINK) tabout(depth, " DESCEND>" + this);
for (LWComponent c : getConnected()) {
depth++; // for debug
try {
if (c != backstop) {
if (c instanceof LWLink && ((LWLink)c).isPrunedBelow(this)) {
bag.add(c);
if (DEBUG.LINK) tabout(depth, " (pruned)" + c); // note: could also be a dupe at this point
} else {
if (DEBUG.LINK) tabout(depth, " include>" + c);
c.getLinkChainImpl(bag, backstop, depth);
}
} else {
// loop-prevention: never traverse through the backstop,
// otherwise the entire map could wind up pruned, such that
// nothing is visible
if (DEBUG.LINK) tabout(depth, "(BACKSTOP)" + c);
}
} catch (Throwable t) {
Log.error("processing " + c + " at depth " + depth, t);
} finally {
depth--; // for debug
}
}
return bag;
}
public Rectangle2D.Float getFanBounds() {
return getFanBounds(null);
}
/** @return the union of the bounds of the current component, all connected links, and all far endpoints
* of those links.
*/
public Rectangle2D.Float getFanBounds(Rectangle2D.Float rect)
{
if (rect == null)
rect = getMapBounds();
else
rect.setRect(getMapBounds());
for (LWLink link : getLinks()) {
final LWComponent head = link.getHead();
final LWComponent tail = link.getTail();
rect.add(link.getPaintBounds());
if (head != this) {
if (head != null)
rect.add(head.getPaintBounds());
} else if (tail != this) {
if (tail != null)
rect.add(tail.getPaintBounds());
}
}
return rect;
}
public Rectangle2D.Float getCenteredFanBounds() {
return expandToCenteredBounds(getFanBounds());
}
/** get bounds that are centered on this node that fully include the given bounds */
public Rectangle2D.Float expandToCenteredBounds(Rectangle2D.Float r) {
// expand the given rectangle in all directions such that the distance
// from our center point of this component to each edge is the same.
final float cx = getMapCenterX();
final float cy = getMapCenterY();
final float topDiff = cy - r.y;
final float botDiff = (r.y + r.height) - cy;
final float leftDiff = cx - r.x;
final float rightDiff = (r.x + r.width) - cx;
if (topDiff > botDiff) {
// expand below us
r.height = topDiff * 2;
} else if (botDiff > topDiff) {
// expand above us
r.y = cy - botDiff;
r.height = botDiff * 2;
}
if (leftDiff > rightDiff) {
// expand to the right
r.width = leftDiff * 2;
} else if (rightDiff > leftDiff) {
// expand to the left
r.x = cx - rightDiff;
r.width = rightDiff * 2;
}
return r;
}
/*
* Return an iterator over all link endpoints,
* which will all be instances of LWComponent.
* If this is a LWLink, it should include it's
* own endpoints in the list.
public java.util.Iterator<LWComponent> getLinkEndpointsIterator()
{
return
new java.util.Iterator<LWComponent>() {
java.util.Iterator i = getLinkRefs().iterator();
public boolean hasNext() {return i.hasNext();}
public LWComponent next()
{
LWLink l = (LWLink) i.next();
LWComponent head = l.getHead();
LWComponent tail = l.getTail();
// Every link, as it's connected to us, should have us as one of
// it's endpoints -- so return the opposite endpoint. TODO: now
// that links can have null endpoints, this iterator can return null
// -- hasNext will have to get awfully fancy to handle this.
if (head == LWComponent.this)
return tail;
else
return head;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
*/
/* include all links and far endpoints of links connected to this component
public java.util.List getAllConnectedComponents()
{
List list = new java.util.ArrayList(mLinks.size());
for (LWLink l : mLinks) {
list.add(l);
if (l.getHead() != this)
list.add(l.getHead());
else if (l.getTail() != this) // todo opt: remove extra check eventually
list.add(l.getTail());
else
// todo: actually, I think we want to support these
throw new IllegalStateException("link to self on " + this);
}
return list;
}
*/
public int countLinksTo(LWComponent c)
{
if (c == null || mLinks == null)
return 0;
int count = 0;
for (LWLink link : mLinks)
if (link.hasEndpoint(c))
count++;
return count;
}
/** @return true if there are any links between us and the given component */
public boolean hasLinkTo(LWComponent c)
{
if (c == null || mLinks == null)
return false;
for (LWLink link : mLinks)
if (link.hasEndpoint(c))
return true;
return false;
}
/** @return true if there are any links between us and the given component */
public boolean hasDirectedLinkTo(LWComponent c)
{
if (c == null || mLinks == null)
return false;
for (LWLink link : mLinks)
{
LWComponent tail = link.getTail();
if (link.hasEndpoint(c) && tail.equals(c))
return true;
}
return false;
}
/** @return true if there are any links between us and the given component */
public boolean hasMultipleLinksTo(LWComponent c)
{
if (c == null || mLinks == null)
return false;
int count = 0;
for (LWLink link : mLinks)
if (link.hasEndpoint(c))
count++;
if (count >1)
return true;
else
return false;
}
/** @return true of this component has any connections (links) to the given component.
* LWLink overrides to include it's endpoints in the definition of "connected" to.
*/
public boolean isConnectedTo(LWComponent c) {
return hasLinkTo(c);
}
public int countCurvedLinksTo(LWComponent c)
{
int count = 0;
for (LWLink link : getLinks())
if (link.hasEndpoint(c) && link.isCurved())
count++;
return count;
}
/** supports ensure link paint order code */
protected LWComponent getParentWithParent(LWContainer parent)
{
if (getParent() == parent)
return this;
if (getParent() == null)
return null;
return getParent().getParentWithParent(parent);
}
/** @return a collection of our ancestors. default impl returns a list with nearest ancestor first */
public List<LWComponent> getAncestors() {
return (List) getAncestors(new ArrayList(8));
}
protected Collection<LWComponent> getAncestors(Collection bag) {
if (parent != null) {
bag.add(parent);
return parent.getAncestors(bag);
} else
return bag;
}
public boolean hasAncestor(LWComponent c) {
final LWComponent parent = getParent();
if (parent == null)
return false;
else if (c == parent)
return true;
else
return parent.hasAncestor(c);
}
public boolean hasAncestorOfType(Class clazz) {
return getParentOfType(clazz) != null;
}
/** @return the first ancestor, EXCLUDING this component (starting with the parent), that is of the given type, or null if none found */
public <T extends LWComponent> T getParentOfType(Class<T> clazz) {
return getParentOfType(clazz, null);
}
/** never ascend above root */
public <T extends LWComponent> T getParentOfType(Class<T> clazz, LWComponent root) {
LWComponent parent = getParent();
if (parent == null)
return null;
else
return parent.getAncestorOfType(clazz, root);
}
/** @return the first ancestor, INCLUDING this component, that is of the given type, or null if none found */
// TODO: including this component is confusing...
public <T extends LWComponent> T getAncestorOfType(Class<T> clazz) {
return getAncestorOfType(clazz, null);
}
/** never ascend above root */
public <T extends LWComponent> T getAncestorOfType(Class<T> clazz, LWComponent root) {
if (clazz.isInstance(this))
return (T) this;
else if (this == root)
return null;
else
return getParentOfType(clazz, root);
}
public LWComponent getTopMostAncestorOfType(Class clazz) {
return getTopMostAncestorOfType(clazz, null);
}
/** never ascend above root */
public LWComponent getTopMostAncestorOfType(Class clazz, LWComponent root) {
LWComponent topAncestor = getAncestorOfType(clazz, root);
LWComponent nextAncestor = topAncestor;
if (nextAncestor != null) {
for (;;) {
nextAncestor = nextAncestor.getParentOfType(clazz, root);
if (nextAncestor != null)
topAncestor = nextAncestor;
else
break;
//if (DEBUG.PICK) out("nextAncestor of type " + clazz + ": " + topAncestor);
}
}
return topAncestor;
}
/** @return by default, return the class object as returned by getClass(). Subclasses can override to provide differentiation between runtime sub-types.
* E.g., a node class could return getClass() by default, but the constant string "textNode" for runtime instances that we
* want the tool code to treat is coming from a different class. Also note that supported property bits for
* all instances with a given type token should be the same.
*/
public Object getTypeToken() {
// todo: should really return null if we detect this is an instance of an anonymous class
// -- we don't want to be duplicating and using a style holder an instance of an anon
// glass that might be overriding god knows what and affecting property setting/getting
// Not that this will probably hurt anything: it'll never be referenced by a VueTool,
// so we'll never see it even if it winds up in the typed style cache.
return getClass();
}
/** @return the viewer margin in pixels when we're the focal -- default is 30 */
public int getFocalMargin() {
return 30;
}
protected void takeScale(double newScale) {
if (DEBUG.LAYOUT) out("takeScale " + newScale);
this.scale = newScale;
}
protected void setScale(double newScale)
{
if (this.scale == newScale)
return;
final double oldScale = this.scale;
//if (DEBUG.LAYOUT) out("setScale " + newScale);
//if (DEBUG.LAYOUT) tufts.Util.printClassTrace("tufts.vue", "setScale " + scale);
takeScale(newScale);
// can only do this via debug inspector right now, and is causing lots of
// suprious events during init:
//if (LWLink.LOCAL_LINKS && !mXMLRestoreUnderway)
if (!mXMLRestoreUnderway)
notify(LWKey.Scale, oldScale); // todo: make scale a real property
updateConnectedLinks(null);
//System.out.println("Scale set to " + scale + " in " + this);
}
/**
* @return the scale value relative to it's parent. So for a 50% scale in it's parent,
* it just returns 0.5. E.g., this would mean if the parent was also scaled at 50%,
* the net on-map visible scaled size of the component would be 25%.
*/
public double getScale()
{
return this.scale;
}
/** @return the on-map scale at 100% map scale (the concatentation of our scale plus all parent scales) */
public double getMapScale()
{
if (getParent() == null)
return getScale();
else
return getParent().getMapScale() * getScale();
}
/** Convenience for returning float */ public final float getScaleF() { return (float) getScale(); }
/** Convenience for returning float */ public final float getMapScaleF() { return (float) getMapScale(); }
public Size getMinimumSize() {
return MinSize;
}
public void setFrame(Rectangle2D r)
{
setFrame((float)r.getX(), (float)r.getY(),
(float)r.getWidth(), (float)r.getHeight());
}
/**
* Default impl just call's setSize, then setLocation. You
* may want to override if want to constrain in some way,
* such as to underlying content (e.g., an image).
*/
public void setFrame(float x, float y, float w, float h)
{
if (DEBUG.LAYOUT) out("*** setFrame " + x+","+y + " " + w+"x"+h);
setSize(w, h); // todo: can use setSizeImpl w/internal flag?
setLocation(x, y);
/*
Object old = new Rectangle2D.Float(this.x, this.y, getWidth(), getHeight());
takeLocation(x, y);
takeSize(w, h);
updateConnectedLinks();
notify(LWKey.Frame, old);
*/
}
/** default calls setFrame -- override to provide constraints */
public void userSetFrame(float x, float y, float w, float h) {
setFrame(x, y, w, h);
}
protected void userSetFrame(float x, float y, float w, float h, MapMouseEvent e) {
userSetFrame(x, y, w, h);
}
// todo: handle via disabling a location property?
public void setMoveable(boolean moveable) {
setFlag(Flag.FIXED_LOCATION, !moveable);
}
public boolean isMoveable() {
return hasFlag(Flag.FIXED_LOCATION) == false;
}
/** @return true if this component is "owned" by the pathway -- e.g., a slide that only appears as an icon */
public boolean isPathwayOwned() {
return false;
}
//private boolean linkNotificationDisabled = false;
protected void takeLocation(float x, float y) {
if (DEBUG.LAYOUT) {
out("takeLocation " + x + "," + y);
//if (DEBUG.META) tufts.Util.printStackTrace("takeLocation");
}
if (x != x || y != y) { // checking for Float.NaN
String msg = "bad location: " + x + "," + y + " for " + this;
if (DEBUG.Enabled) {
Log.warn(msg, new Throwable("HERE"));
//System.exit(-1);
} else {
Log.warn(msg);
}
if (x == x)
this.x = x;
if (y == y)
this.y = y;
} else {
this.x = x;
this.y = y;
}
}
// public void userTranslate(float dx, float dy) {
// translate(dx, dy);
// }
/** Translate this component within it's parent by the given amount */
public void translate(float dx, float dy) {
setLocation(this.x + dx,
this.y + dy);
}
/** Translate this component within it's parent by the given amount -- quietly w/out generating events */
public void takeTranslation(float dx, float dy) {
takeLocation(this.x + dx,
this.y + dy);
}
// // moved to LWGroup -- currently only usage point
// /** translate across the map in absolute map coordinates */
// public void translateOnMap(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 = getParent().getMapScale();
// if (scale != 1.0) {
// dx /= scale;
// dy /= scale;
// }
// translate((float) dx, (float) dy);
// }
/** set the absolute map location -- meant to be overriden for special cases (e.g., the special selection group) */
public void setMapLocation(double x, double y) {
throw new UnsupportedOperationException("unimplemented in " + this);
// final double scale = getMapScale();
// out("map scale: " + scale);
// if (scale != 1.0) {
// final double oldMapX = getMapX();
// final double oldMapY = getMapY();
// final double dx = (x - oldMapX) * scale;
// final double dy = (y - oldMapY) * scale;
// setLocation((float) (oldMapX + dx),
// (float) (oldMapY + dy));
// } else
// setLocation((float) x, (float) y);
}
/**
* Set the location of this object within it's parent. E.g., if the parent is a group or a slide,
* setLocation(0,0) would move the component to the upper left corner of it's parent. If the
* parent is a map, (0,0) has no special meaning as the origin of Maps, while it does exist,
* has no special meaning when they draw.
*/
public void setLocation(float x, float y) {
setLocation(x, y, this, true);
}
/** Special setLocation to permit event notification during coordinate system changes for objects not yet added to the map */
protected void setLocation(float x, float y, LWComponent hearableEventSource, boolean issueMapLocationChangeCalls)
{
if (this.x == x && this.y == y)
return;
final Point2D.Float oldValue = new Point2D.Float(this.x, this.y);
takeLocation(x, y);
//if (!linkNotificationDisabled)
// updateConnectedLinks();
if (hearableEventSource != this)
hearableEventSource.notifyProxy(new LWCEvent(hearableEventSource, this, LWKey.Location, oldValue));
else //if (hearableEventSource != null) // if null, skip event delivery
notify(LWKey.Location, oldValue);
// if (issueMapLocationChangeCalls && parent != null) {
if (issueMapLocationChangeCalls) {
// NEED TO DEAL WITH COORDINATE SYSTEM CHANGES
// And need to be able to capture old map location from our OLD parent
// during reparenting....
// reparenting may want to force a location in the new parent, at it's
// current map location, but relative to the new parent's location,
// even if it's about to be moved/laid-out elsewhere, so that once
// we get here, the below code should always work. Or, we could
// even have establishLocalCoordinates call us here with extra info... (oldMapX/oldMapY)
// or, we could implement the general setMapLocation and have establishLocalCoords call that...
// This code only works if we're moving within a single parent: no coordinate system changes!
// Would be better to merge this somehow with notifyHierarchChanged?
final double scale;
if (parent != null)
scale = parent.getMapScale(); // we move within the scale of our parent
else
scale = 1.0;
if (DEBUG.LAYOUT) out("notifyMapLocationChanged: using scale " + scale);
notifyMapLocationChanged(this,
(x - oldValue.x) * scale,
(y - oldValue.y) * scale);
} else {
// this always needs to happen no matter what, even during undo
// (e.g., the shape of curves isn't stored anywhere -- always needs to be recomputed)
//if (!linkNotificationDisabled)
if (updatingLinks())
updateConnectedLinks(this);
}
}
/**
* Tell all links that have us as an endpoint that we've
* moved or resized so the link knows to recompute it's
* connection points.
*/
protected void updateConnectedLinks(LWComponent movingSrc)
{
//if (!linkNotificationDisabled) // todo: if still end up using this feature, need to pass this bit on down to children
if (updatingLinks())
if (mLinks != null && mLinks.size() > 0)
for (LWLink link : mLinks)
link.notifyEndpointMoved(movingSrc, this);
}
// boolean isFocal;
// void setFocal(boolean isFocal) {
// this.isFocal = isFocal;
// }
/** a notification to the component that it's absolute map location has changed by the given absolute map dx / dy */
// todo: may be better named ancestorMoved or ancestorTranslated or some such
protected void notifyMapLocationChanged(LWComponent movingSrc, double mdx, double mdy) {
//if (!linkNotificationDisabled) // todo: if still end up using this feature, need to pass this bit on down to children
if (updatingLinks())
updateConnectedLinks(movingSrc);
}
protected void notifyMapScaleChanged(double oldParentMapScale, double newParentMapScale) {}
// /** A notification to the component that it or some ancestor is about to change parentage */
// public void notifyHierarchyChanging() {}
/** A notification to the component that it or some ancestor changed parentage */
public void notifyHierarchyChanged() {
if (mLinks != null && mLinks.size() > 0)
for (LWLink link : mLinks)
link.notifyEndpointHierarchyChanged(this);
}
public final void setLocation(double x, double y) {
setLocation((float) x, (float) y);
}
public final void setLocation(Point2D p) {
setLocation((float) p.getX(), (float) p.getY());
}
/** default calls setLocation -- override to provide constraints */
public void userSetLocation(float x, float y) {
setLocation(x, y);
}
public void setCenterAt(Point2D p) {
setCenterAt(p.getX(), p.getY());
}
public void setCenterAt(double x, double y) {
setLocation((float) x - getWidth()/2,
(float) y - getHeight()/2);
}
public Point2D getLocation()
{
return new Point2D.Float(getX(), getY());
}
/** set component to this many pixels in size, quietly, with no event notification */
protected void takeSize(float w, float h)
{
//if (this.width == w && this.height == h)
//return;
if (DEBUG.LAYOUT) out("*** takeSize (LWC) " + w + "x" + h);
this.width = w;
this.height = h;
}
protected float mAspect = 0;
public void setAspect(float aspect) {
mAspect = aspect;
if (DEBUG.IMAGE) out("setAspect " + aspect);
}
/** set component to this many pixels in size */
public final void setSize(float w, float h) {
setSizeImpl(w, h, false);
}
/** set component to this many pixels in size
* @param intetrnal -- if true, the event is not undoable
*/
protected void setSizeImpl(float w, float h, boolean internal)
{
if (this.width == w && this.height == h)
return;
if (DEBUG.LAYOUT) out("*** setSize (LWC) " + w + "x" + h);
final boolean quiet = (this.width == NEEDS_DEFAULT);
//final boolean skipUndo = internal;
final boolean skipUndo;
if (!internal && !javax.swing.SwingUtilities.isEventDispatchThread()) {
// There was a reason this was important to do -- I think in some cases this could
// leave undoable state in the undo queue after sizes changes that were really from
// initializations, and we do NOT want to allow any undo back to a random pre-init
// size. [my guess is that this had/has to do with autosized] TODO 2012: BUG: selection
// format action "fill to width" is no longer capture what it does via undo..
if (DEBUG.THREAD||DEBUG.UNDO||DEBUG.EVENTS) Log.info("skipping undo on non-AWT size change: " + this + "; newSize=" + w + "x" + h);
skipUndo = true;
} else {
skipUndo = internal;
}
final Object old = skipUndo ? LWCEvent.NO_OLD_VALUE : new Size(width, height);
if (mAspect > 0) {
Size constrained = ConstrainToAspect(mAspect, w, h);
w = constrained.width;
h = constrained.height;
}
if (w < MIN_SIZE) w = MIN_SIZE;
if (h < MIN_SIZE) h = MIN_SIZE;
takeSize(w, h);
if (isLaidOut())
getParent().layout();
updateConnectedLinks(null);
if (!quiet && isAutoSized())
notify(LWKey.Size, old); // technically only needed if is user-sized (otherwise layout code handles this)
}
public static Size ConstrainToAspect(double aspect, double w, double h)
{
if (DEBUG.IMAGE) Log.debug("constrainToAspect " + aspect + " " + w + "x" + h);
// Given width & height are MINIMUM size: expand to keep aspect
if (w <= 0) w = 1;
if (h <= 0) h = 1;
double tmpAspect = w / h; // aspect we would have if we did not constrain it
//if (DEBUG.IMAGE) Log.debug("ConstrainToAspect " + tmpAspect);
// a = w / h
// w = a*h
// h = w/a
// if (DEBUG.PRESENT || DEBUG.IMAGE) {
// out("keepAspect=" + mAspect);
// out(" tmpAspect=" + tmpAspect);
// }
// if (h == this.height) {
// out("case0");
// h = (float) (w / mAspect);
// } else if (w == this.width) {
// out("case1");
// w = (float) (h * mAspect);
// } else
if (tmpAspect > aspect) {
//out("case2: expand height");
h = w / aspect;
} else if (tmpAspect < aspect) {
//out("case3: expand width");
w = h * aspect;
}
//else out("NO ASPECT CHANGE");
Size s = new Size(w, h);
if (DEBUG.IMAGE) Log.debug("constrainToAspect out: " + s);
return s;
/*
if (false) {
if (h == this.height || tmpAspect < mAspect)
h = (float) (w / mAspect);
else if (w == this.width || tmpAspect > mAspect)
w = (float) (h * mAspect);
} else {
if (tmpAspect < mAspect)
h = (float) (w / mAspect);
else if (tmpAspect > mAspect)
w = (float) (h * mAspect);
}
*/
}
/** default calls setSize -- override to provide constraints */
public void userSetSize(float w, float h) {
setSize(w, h);
}
protected void userSetSize(float w, float h, MapMouseEvent e) {
userSetSize(w, h);
}
/* set on screen visible component size to this many pixels in size -- used for user set size from
* GUI interaction -- takes into account any current scale factor
* (do we still need this? I think this should be deprecated -- SMF)
*/
// public void setAbsoluteSize(float w, float h)
// {
// if (true||DEBUG.LAYOUT) out("*** setAbsoluteSize " + w + "x" + h);
// setSize(w / getScaleF(), h / getScaleF());
// //setSize(w / getMapScaleF(), h / getMapScaleF());
// }
/** for XML restore only -- issues no event updates */
public void setX(float x) { this.x = x; }
/** for XML restore only -- issues no event updates */
public void setY(float y) { this.y = y; }
/** for castor restore -- will not trigger any events */
public void setWidth(float w) { this.width = w; }
/** for castor restore -- will not trigger any events */
public void setHeight(float h) { this.height = h; }
/*
* getMapXXX methods are for values in absolute map positions and scales (needed for VUE.RELATIVE_COORDS == true)
* getScaledXXX methods are for VUE.RELATIVE_COORDS == false, tho I think we can get rid of them? -- SMF
*
* "Map" values are absolute on-screen values that are true for any component in a map rendered at 100% scale (the size & location)
* (better naming scheme might be "getRenderXXX" or "getAbsoluteXX" ?)
*/
public float getX() { return this.x; }
public float getY() { return this.y; }
public float getWidth() { return this.width; }
public float getHeight() { return this.height; }
/** @return the width inside the local parent (width * scale) */
public float getLocalWidth() { return (float) (this.width * getScale()); }
/** @return the height inside the local parent (height * scale) */
public float getLocalHeight() { return (float) (this.height * getScale()); }
/** @return on-map width when viewed at 100% */
public float getMapWidth() { return (float) (this.width * getMapScale()); }
/** @return on-map height when viewed at 100% */
public float getMapHeight() { return (float) (this.height * getMapScale()); }
/** @return local width including any border stroke ((width + stroke) * scale) */
public float getLocalBorderWidth() { return (float) ((this.width + mStrokeWidth.get()) * getScale()); }
/** @return local height including any border stroke ((height + stroke) * scale) */
public float getLocalBorderHeight() { return (float) ((this.height + mStrokeWidth.get()) * getScale()); }
/** convenience */
public Size getSize() {
return new Size(this.width, this.height);
}
protected double getMapXPrecise()
{
if (parent == null) {
//if (DEBUG.Enabled && this instanceof LWMap == false)
// Util.printStackTrace("fetching mapX for unparented non-map: " + this);
return getX();
} else {
return parent.getMapXPrecise() + getX() * parent.getMapScale();
}
}
protected double getMapYPrecise() {
if (parent == null) {
return getY();
} else {
if (parent == this) { // DEBUG
Util.printStackTrace("PARENT LOOP " + this);
return getY();
}
return parent.getMapYPrecise() + getY() * parent.getMapScale();
}
}
public float getMapX() {
return (float) getMapXPrecise();
}
public float getMapY() {
return (float) getMapYPrecise();
}
/** @return center x of the component in absolute map coordinates */
public float getMapCenterX() {
return getMapX() + getMapWidth() / 2;
}
/** @return center y of the component in absolute map coordinates */
public float getMapCenterY() {
return getMapY() + getMapHeight() / 2;
}
/** @return the center of this node in map coordinates */
public Point2D getMapCenter() {
return new Point2D.Float(getMapCenterX(), getMapCenterY());
}
// // these two don't handle scale properly yet: need to adjust for parent scales...
// protected float getCenterX(LWContainer ancestor) {
// return (float) getAncestorX(ancestor) + getScaledWidth() / 2;
// }
// protected float getCenterY(LWContainer ancestor) {
// return (float) getAncestorY(ancestor) + getScaledHeight() / 2;
// }
// // these two don't handle scale properly yet
// public float getLinkConnectionX(LWContainer ancestor) {
// //return getCenterX(ancestor);
// return (float) getAncestorX(ancestor) + getScaledWidth() / 2;
// }
// public float getLinkConnectionY(LWContainer ancestor) {
// //return getCenterY(ancestor);
// return (float) getAncestorY(ancestor) + getScaledHeight() / 2;
// }
protected void getLinkConnectionCenterRelativeTo(Point2D.Float point, LWContainer relative)
{
//if (relative == null) Util.printStackTrace("null relative for " + this + ": " + relative);
if (relative == this) {
if (DEBUG.Enabled)
Util.printStackTrace("debug: " + this + " is computing link connetion center relative to itself");
//final float scale = getMapScaleF();
point.x = getZeroCenterX();
point.y = getZeroCenterY();
//point.x = getZeroCenterX() * scale;
//point.y = getZeroCenterY() * scale;
} else if (relative == null) {
//} else if (relative == null || relative == parent) {
// if relative is null, just return available local data w/out accessing the parent.
// This can happen normally during init.
if (this instanceof LWLink) {
point.x = getZeroCenterX();
point.y = getZeroCenterY();
} else {
// works for connecting to something scaled for a map link to a scaled map-node:
//point.x = getX() + getZeroCenterX() * scale;
//point.y = getY() + getZeroCenterY() * scale;
// works for connecting to inside a scaled context (e.g., a scaled down on-map slide)
point.x = getX() + getZeroCenterX();
point.y = getY() + getZeroCenterY();
}
} else if (true /*|| ROTATE_TEST*/) {
// can we construct a relativing x-hierarchy transformer in one pass?
// e.g., a combination of transformDown's then I guess transformUp's (would need that),
// on an AffineTransform, should produce a x-hierarchy transformer.
// Anyway, this is the safest method possible: transform up to the map,
// then back down to the other context, taking no shortcuts.
point.x = getZeroCenterX();
point.y = getZeroCenterY();
if (DEBUG.LINK) out(" ZeroCenter: " + point);
transformZeroToMapPoint(point, point);
if (DEBUG.LINK) out(" MapCenter: " + point);
relative.transformMapToZeroPoint(point, point);
if (DEBUG.LINK) out("RelativeCenter: " + point + " to " + relative);
} else {
// note that this is the NET scale -- scale effective at the map level
// -- THIS ISN'T CORRECT -- we need the scale relative to relative...
final float scale = getMapScaleF();
if (this instanceof LWLink) {
// todo: consider getMapX/Y on LWLink override to return getParent().getMapX/Y (need to check all calls tho...)
point.x = parent.getMapX() + getZeroCenterX() * scale;
point.y = parent.getMapY() + getZeroCenterY() * scale;
} else {
point.x = getMapX() + getZeroCenterX() * scale;
point.y = getMapY() + getZeroCenterY() * scale;
}
// point now has map coords -- now make relative to desired component
// (the x/y needed if drawn in the component, that produces the same
// ultimate map location). Normally, relative should always
// be one of our ancestors, as this is for special link code that
// should only ever be interested in an ancestor value, tho we compute
// it generically just in case.
if (DEBUG.Enabled) {
if (relative != null && !hasAncestor(relative)) {
// only if not the special invisible link endpoint, which has no parent (thus no ancestors)
if (getClass().getEnclosingClass() != LinkTool.LinkModeTool.class) {
//String msg = "debug: " + this + " is computing link connetion center relative to a non-ancestor: " + relative;
String msg = "non-ancestor: " + relative + " used as parent-relative of " + this;
if (DEBUG.META)
Util.printStackTrace(msg);
else
Log.debug(msg);
}
}
}
relative.transformMapToZeroPoint(point, point);
}
}
/** @return our center in our zero-based coordinate space: e.g., 1/2 our width. Links
* will compute differentely, as their zero-based coordinate space is their parent's space (same as local space) */
protected float getZeroCenterX() {
return getWidth() / 2;
}
protected float getZeroCenterY() {
return getHeight() / 2;
}
//-----------------------------------------------------------------------------
// experimental relatve-to-a-given-ancestor coord fetchers
// TODO: NOT WORTH THE TROUBLE RIGHT NOW OF USING THE ANCESTOR OPTIMIZATION:
// Just get the freakin mapx of the desired relative-to component --
// someday those values may be cached in the object/transform anyway.
// Oh tho -- I think in LWLink we need the mapX of US, plus the mapX of the target
// (if KEEP the ancestor code, implement generically so can pass in any value: e.g, LWLink.mCurveCenterX)
//-----------------------------------------------------------------------------
protected double getAncestorX(LWContainer ancestor) {
if (ancestor == parent) // quick check for the common case
return getX();
else if (parent == null) {
Util.printStackTrace("didn't find ancestor " + ancestor + " for " + this);
return getX();
} else
return parent.getAncestorX(ancestor) + getX() * parent.getMapScale();
}
protected double getAncestorY(LWContainer ancestor) {
if (ancestor == parent) // quick check for the common case
return getY();
else if (parent == null) {
Util.printStackTrace("didn't find ancestor " + ancestor + " for " + this);
return getY();
} else
return parent.getAncestorY(ancestor) + getY() * parent.getMapScale();
}
// protected double ancestorY(double y, LWContainer ancestor) {
// if (ancestor == parent) // quick check for the common case
// return y;
// else if (parent == null) {
// Util.printStackTrace("didn't find ancestor " + ancestor + " for " + this);
// return y;
// } else
// return parent.ancestorY(y, ancestor) + getY() * parent.getMapScale();
// }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/**
* @return java.net.URI
*
* returns a unique URI for a component. If component already has one it is returned else an new uri is created and returned.
* Would be nice if these were somehow persistenly unique via specified naming authorities. As they're currently
* not, this is kind of overkill for simple runtime unique indexing string lookups. These *are* persisted in
* in save files, tho I'm not sure if there's any point in doing so.
*/
public java.net.URI getURI() {
//if (isStyle) return null;
if (uri == null) {
try {
uri = new URI(edu.tufts.vue.rdf.RDFIndex.getUniqueId());
} catch (Throwable t) {
tufts.Util.printStackTrace(t, "Failed to create an uri for "+label);
}
}
return uri;
}
public void setURI(URI uri) {
// if (isStyle) {
// VUE.Log.warn("attempt to set URI on a style: " + this + "; uri=" + uri);
// return;
// }
this.uri = uri;
}
/* Methods to persist url through castor
* We don't want to save URI object
*
*/
public void setURIString(String URIString) {
// if (isStyle) {
// VUE.Log.warn("attempt to set URIString on a style: " + this + "; uriString=" + uri);
// return;
// }
try {
uri = new URI(URIString);
//edu.tufts.vue.rdf.VueIndexedObjectsMap.setID(uri,this);
} catch (Throwable t) {
tufts.Util.printStackTrace(t, "Failed to set an uri for "+label);
}
}
public String getURIString() {
return getURI().toString();
}
/*
public void setShape(Shape shape)
{
throw new UnsupportedOperationException("unimplemented setShape in " + this);
}
*/
/** @return our shape, full transformed into map coords and ultimate scale when drawn at 100% map zoom
* this is used for portal clipping, and will be imperfect for some scaled shapes, such as RountRect's
* This only works for raw shapes that are RectangularShapes -- other Shape types just return the bounding
* box in map coordinates (e.g., a link shape)
*/
public RectangularShape getMapShape()
{
// Will not work for shapes like RoundRect when scaled -- e..g, corner scaling will be off
final Shape s = getZeroShape();
// if (getMapScale() != 1f && s instanceof RectangularShape) { // todo: do if any transform, not just scale
if (s instanceof RectangularShape) {
// todo: cache this: only need to updaate if location, size or scale changes
// (Also, on the scale or location change of any parent!)
RectangularShape rshape = (RectangularShape) s;
rshape = (RectangularShape) rshape.clone();
AffineTransform a = getZeroTransform();
Point2D.Float loc = new Point2D.Float();
a.transform(loc, loc);
rshape.setFrame(loc.x, loc.y,
rshape.getWidth() * a.getScaleX(),
rshape.getHeight() * a.getScaleY());
//System.out.println("TRANSFORMED SHAPE: " + rshape + " for " + this);
return rshape;
} else {
return getMapBounds();
}
}
/** @return the raw shape of this object, not including any shape (the stroke is laid on top of the raw shape).
This is the zero based non-scaled shape (always at 0,0) */
private Shape getShape()
{
return getZeroShape();
}
protected Rectangle2D.Float mZeroBounds; // don't pre-allocate -- won't be used by overriding impl's
/** @return the raw, zero based, non-scaled shape; default impl returns same as getZeroBounds */
public Shape getZeroShape() {
if (mZeroBounds == null)
mZeroBounds = new Rectangle2D.Float();
mZeroBounds.width = getWidth();
mZeroBounds.height = getHeight();
return mZeroBounds;
}
/**
* @return the raw, zero based, non-scaled bounds.
*
* Altho the x/y of the rectangle will normally be 0,0 (suggesting we could just use
* a size object here), that's not always the case: a component who shares it's
* coordinate space with it's parent (such as a link) will usually have a non-zero
* x/y in the zero bounds.
*/
protected Rectangle2D.Float getZeroBounds() {
return new Rectangle2D.Float(0, 0, getWidth(), getHeight());
}
// protected Size getZeroPaintSize() {
// final float strokeWidth = getStrokeWidth()l
// if (strokeWidth > 0) {
// return new Size(getWidth() + strokeWidth, getHeight() + strokeWidth);
// } else {
// return new Size(getWidth(), getHeight());
// }
// }
/** @return the PARENT based bounds -- this is the local component x,y width*scale,height*scale, where scale
* is any local scale this component has (not the total map scale: the scale that includes the scaling of all ancestors) */
public Rectangle2D.Float getLocalBounds() {
return new Rectangle2D.Float(getX(), getY(), getLocalWidth(), getLocalHeight());
}
/** @return the layout bounds -- this is the local bounds, plus an extra "hangoff" decorations that are not considered
* part of the formal bounds of the object. When the object is the focal, these items are not displayed.
*/
public Rectangle2D.Float getLayoutBounds() {
return getLocalBounds();
}
/** @return the local (parent-based) border bounds */
public Rectangle2D.Float getLocalBorderBounds() {
return addLocalStrokeToBounds(getLocalBounds());
}
/** @return the PARENT based, non-scaled bounds including all extra-shape artifacts, such as a stroke */
public Rectangle2D.Float getLocalPaintBounds() {
return addStrokeToBounds(getLocalBounds(), 0f);
}
/** @return getMapBounds() -- map-coord (absolute) bounds of the stroke shape (not including any stroke width) */
public final Rectangle2D.Float getBounds()
{
return getMapBounds();
}
/** @return map-coord (absolute) bounds of the stroke shape (not including any stroke width) */
public Rectangle2D.Float getMapBounds()
{
return new Rectangle2D.Float(getMapX(), getMapY(), getMapWidth(), getMapHeight());
}
/**
* Return absolute map bounds for hit detection & clipping. This will vary
* depenending on current stroke width, if in a visible pathway,
* etc.
*/
public Rectangle2D.Float getPaintBounds()
{
if (LWPathway.isShowingSlideIcons() && inDrawnPathway()) {
Rectangle2D.Float b = addStrokeToBounds(getMapBounds(), LWPathway.PathBorderStrokeWidth);
if (farthestVisibleSlideCorner != null) {
//if (DEBUG.WORK) out("IN DRAWN PATHWAY w/CORNER");
b.add(farthestVisibleSlideCorner);
}
return b;
} else
return addStrokeToBounds(getMapBounds(), 0);
}
/** @return bounds to use when this is the focal */
public Rectangle2D.Float getFocalBounds() {
// do not include any slide icons
return addStrokeToBounds(getMapBounds(), this instanceof LWImage ? 0 : 25);
//return getFanBounds(new Rectangle2D.Float());
}
/**
* Return absolute map bounds including any border stroke -- used by Groups.
*/
public Rectangle2D.Float getBorderBounds()
{
return addStrokeToBounds(getMapBounds(), 0);
}
/** take the given map bounds, and add the scaled stroke width plus any extra if given */
private Rectangle2D.Float addStrokeToBounds(Rectangle2D.Float r, float extra)
{
float strokeWidth = getStrokeWidth() + extra;
if (strokeWidth > 0) {
strokeWidth *= getMapScale();
final float exteriorStroke = strokeWidth / 2;
r.x -= exteriorStroke;
r.y -= exteriorStroke;
r.width += strokeWidth;
r.height += strokeWidth;
}
// we need this adjustment for repaint optimzation to
// work properly -- would be a bit cleaner to compensate
// for this in the viewer
//if (isIndicated() && STROKE_INDICATION.getLineWidth() > strokeWidth)
// strokeWidth += STROKE_INDICATION.getLineWidth();
return r;
}
private Rectangle2D.Float addLocalStrokeToBounds(Rectangle2D.Float r)
{
float strokeWidth = getStrokeWidth();
if (strokeWidth > 0) {
strokeWidth *= getScale();
final float exteriorStroke = strokeWidth / 2;
r.x -= exteriorStroke;
r.y -= exteriorStroke;
r.width += strokeWidth;
r.height += strokeWidth;
}
return r;
}
/** @return an AffineTransform that when applied to a graphics context, will have us drawing properly
* relative to this component, including any applicable scaling. So after this is applied,
* 0,0 will draw in the upper left hand corner of the component */
//create and recursively set a transform to get from the Map to this object's coordinate space
// note: structure is same in the different transform methods
// TODO OPT: can cache this transform: if track all ancestor hierarcy, location AND scale changes,
// can skip recomputing it each time.
public final AffineTransform getZeroTransform() {
return loadZeroTransform(_zeroTransform);
// final AffineTransform a;
// if (parent == null) {
// a = new AffineTransform();
// } else {
// a = parent.getZeroTransform();
// }
// return transformDownA(a);
}
protected final AffineTransform loadZeroTransform(final AffineTransform a) {
if (parent == null) {
a.setToIdentity();
return transformDownA(a);
} else {
return transformDownA(parent.loadZeroTransform(a));
}
}
/**
* @return the transform that takes us from the given ancestor down to our local coordinate space/scale
* @param ancestor -- the ancestor to get a transform relative to. If null, this will return the
* same result as getLocalTransform (relative to the map)
*/
protected AffineTransform getRelativeTransform(LWContainer ancestor) {
if (parent == ancestor || parent == null)
return transformDownA(new AffineTransform());
else
return transformDownA(parent.getRelativeTransform(ancestor));
}
final boolean isZoomedFocus() {
return mTemporaryTransform != null;
}
/**
* Called by model clients (e.g., MapViewer) for temporarily applying a special transform to the
* drawing and picking of a component without touching the underlying data model (no persistent
* changes to the component are made). The transform must be set to null to be cleared.
* This is what zoom-rollover uses to temporarily zoom-up a node.
*/
void setZoomedFocus(AffineTransform tx) {
mTemporaryTransform = tx;
//linkNotificationDisabled = isZoomedFocus;
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//
// transformDownA + transformDownG are the two core routines that everything
// ultimately uses -- e.g., placing a test rotation in these methods makes
// it work everywhere that's using the transformation code (drawing, picking,
// and link connections)
//
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/**
* Transform the given AffineTransform down from our parent to us, the child.
*/
protected AffineTransform transformDownA(final AffineTransform a)
{
if (mTemporaryTransform != null) {
a.concatenate(mTemporaryTransform);
} else {
a.translate(this.x, this.y);
if (this.scale != 1)
a.scale(this.scale, this.scale);
}
return a;
}
/** transform relative to the child after already being transformed relative to the parent */
protected void transformDownG(final Graphics2D a)
{
if (mTemporaryTransform != null) {
a.transform(mTemporaryTransform);
} else {
a.translate(this.x, this.y);
if (this.scale != 1)
a.scale(this.scale, this.scale);
}
}
// /** set by model clients (e.g., MapViewer) for the zoomed rollover component */
// private static double ZoomRolloverScale;
// void setZoomedFocus(double zoomFactor) {
// if (zoomFactor > 0) {
// isZoomedFocus = true;
// ZoomRolloverScale = zoomFactor;
// } else {
// isZoomedFocus = false;
// }
// //linkNotificationDisabled = isZoomedFocus;
// }
// private final static boolean ROTATE_TEST = false;
// private static final int RotSteps = 180;
// private static final double RotStep = Math.PI * 2 / RotSteps;
// private static int RotCount = 0;
// /**
// * Transform the given AffineTransform down from our parent to us, the child.
// */
// protected AffineTransform transformDownA(final AffineTransform a)
// {
// if (ROTATE_TEST && parent instanceof LWMap) {
// // rotate around center (relative to map-bounds)
// final double hw = getWidth() / 2;
// final double hh = getHeight() / 2;
// a.translate(getX() + hw, getY() + hh);
// a.scale(scale, scale);
// a.rotate(Math.PI / 8);
// a.translate(-hw, -hh);
// } else {
// if (isZoomedFocus) {
// if (false && this instanceof LWSlide) {
// final double scale = SlideIconScale * 2;
// a.scale(scale, scale);
// } else if (true) {
// a.concatenate(ZoomRolloverTransform);
// } else {
// // Zoom on-center.
// // To make this simple, we first translate to the local center (our
// // center location in parent coords, compensating for any of our own
// // scale), then apply the new zoomed scale, then translate back out
// // by our raw width. This isn't done often, so no point in over
// // optimizing.
// final double halfWidth = getWidth() / 2;
// final double halfHeight = getHeight() / 2;
// final double ourScale = getScale();
// // Translate to local center:
// a.translate(getX() + halfWidth * ourScale,
// getY() + halfHeight * ourScale);
// if (DEBUG.VIEWER) {
// // note that due to nature of this testing uber-hack, the more
// // children something has, the faster it rotates.
// a.rotate(RotStep * RotCount);
// if (++RotCount >= RotSteps)
// RotCount = 0;
// }
// // Set the super-zoom scale:
// //a.scale(ZoomRolloverScale, ZoomRolloverScale);
// a.translate(-halfWidth, -halfHeight);
// }
// } else {
// //-------------------------------------------------------
// // This is the default, standard case:
// //-------------------------------------------------------
// a.translate(this.x, this.y);
// if (this.scale != 1)
// a.scale(this.scale, this.scale);
// }
// }
// return a;
// }
// // // When working on transformDownA, comment this code in, and comment out transformDownG
// // /** Must include overrides of all AffineTransform methods used in transformDownA */
// // private static final class GCAffineProxy extends AffineTransform {
// // private Graphics2D g;
// // @Override
// // public void translate(double x, double y) { g.translate(x, y); }
// // @Override
// // public void scale(double xs, double ys) { g.scale(xs, ys); }
// // @Override
// // public void rotate(double t) { g.rotate(t); }
// // @Override
// // public void concatenate(AffineTransform tx) { g.transform(tx); }
// // }
// // private static final GCAffineProxy GCAP = new GCAffineProxy();
// // /** transform relative to the child after already being transformed relative to the parent */
// // protected void transformDownG(final Graphics2D g) {
// // GCAP.g = g; // not exactly thread-safe -- this temporary while we work on this code (cut/paste duplicate when done)
// // transformDownA(GCAP);
// // }
// /** transform relative to the child after already being transformed relative to the parent */
// protected void transformDownG(final Graphics2D a)
// {
// //-----------------------------------------------------------------------------
// // NOTE THAT THE CODE IN THIS METHOD IS A PURE DUPLICATE OF transformDownA
// // That is, it is literally a cut & paste of the body of transformDownA.
// // The only difference is that our argument is of type Graphics2D, instead
// // of AffineTransform -- we only call methods common to both classes.
// // (and we don't return the passed in argument in this method)
// //-----------------------------------------------------------------------------
// if (ROTATE_TEST && parent instanceof LWMap) {
// // rotate around center (relative to map-bounds)
// final double hw = getWidth() / 2;
// final double hh = getHeight() / 2;
// a.translate(getX() + hw, getY() + hh);
// a.scale(scale, scale);
// a.rotate(Math.PI / 8);
// a.translate(-hw, -hh);
// } else {
// if (false && isZoomedFocus) {
// if (false && this instanceof LWSlide) {
// final double scale = SlideIconScale * 2;
// a.scale(scale, scale);
// } else {
// // Zoom on-center.
// // To make this simple, we first translate to the local center (our
// // center location in parent coords, compensating for any of our own
// // scale), then apply the new zoomed scale, then translate back out
// // by our raw width. This isn't done often, so no point in over
// // optimizing.
// final double halfWidth = getWidth() / 2;
// final double halfHeight = getHeight() / 2;
// final double ourScale = getScale();
// // Translate to local center:
// a.translate(getX() + halfWidth * ourScale,
// getY() + halfHeight * ourScale);
// if (DEBUG.VIEWER) {
// // note that due to nature of this testing uber-hack, the more
// // children something has, the faster it rotates.
// a.rotate(RotStep * RotCount);
// if (++RotCount >= RotSteps)
// RotCount = 0;
// }
// // Set the super-zoom scale:
// //a.scale(ZoomRolloverScale, ZoomRolloverScale);
// a.translate(-halfWidth, -halfHeight);
// }
// } else {
// //-------------------------------------------------------
// // This is the default, standard case:
// //-------------------------------------------------------
// a.translate(this.x, this.y);
// if (this.scale != 1)
// a.scale(this.scale, this.scale);
// }
// }
// }
/** Will transform all the way from the the map down to the component, wherever nested/scaled.
* So drawing at 0,0 will draw in the upper left of the component. */
public void transformZero(final Graphics2D g) {
// todo: need a relative to parent transform only for cascading application during drawing
// (and ultimate picking when impl is optimized)
if (parent == null) {
;
} else {
parent.transformZero(g);
}
transformDownG(g);
}
public Point2D.Float transformMapToZeroPoint(Point2D.Float mapPoint) {
return (Point2D.Float) transformMapToZeroPoint(mapPoint, mapPoint);
}
/**
* @param mapPoint, a point in map coordinates to transform to local coordinates
* @param zeroPoint the destination Point2D to place the resulting transformed coordinate -- may be
* the same object as mapPoint (it will be written over)
* @return the transformed point (will be zeroPoint if transformed, mapPoint if no transformation was needed,
* although mapPoint x/y values should stil be copied to zeroPoint)
*/
public Point2D transformMapToZeroPoint(Point2D.Float mapPoint, Point2D.Float zeroPoint) {
// This doesn't work if we're a link!
// if (!isZoomedFocus && scale == 1.0 && parent instanceof LWMap && !ROTATE_TEST) { // OPTIMIZATION
// zeroPoint.x = mapPoint.x - this.x;
// zeroPoint.y = mapPoint.y - this.y;
// return zeroPoint;
// }
try {
getZeroTransform().inverseTransform(mapPoint, zeroPoint);
} catch (java.awt.geom.NoninvertibleTransformException e) {
Util.printStackTrace(e);
}
return zeroPoint;
}
protected Point2D transformZeroToMapPoint(Point2D.Float zeroPoint, Point2D.Float mapPoint) {
// This doesn't work if we're a link!
// if (!isZoomedFocus && scale == 1.0 && parent instanceof LWMap && !ROTATE_TEST) { // OPTIMIZATION
// mapPoint.x = zeroPoint.x + this.x;
// mapPoint.y = zeroPoint.y + this.y;
// return mapPoint;
// }
getZeroTransform().transform(zeroPoint, mapPoint);
return mapPoint;
}
/**
* @param mapRect -- incoming rectangle to transform to be relative to 0,0 of this component
* @param zeroRect -- result is placed here -- will be created if is null
* @return zeroRect
*
* E.g., if the incoming mapRect was from map coords 100,100->120,120, and this component was at 100,100,
* the resulting zeroRect in this case would be 0,0->20,20 (assuming no scale or rotation).
*
*/
//protected Rectangle2D transformMapToZeroRect(Rectangle2D mapRect, Rectangle2D zeroRect)
protected Rectangle2D transformMapToZeroRect(Rectangle2D mapRect)
{
// if (zeroRect == null)
// zeroRect = (Rectangle2D) mapRect.clone(); // simpler than newInstace, tho we won't need the data-copy in the end
Rectangle2D zeroRect = new Rectangle2D.Float();
// If want to handle rotation, we'll need to transform each corner of the
// rectangle separately, generating Polygon2D (which sun never implemented!) or
// a GeneralPath, in either case changing this method to return a Shape. Better
// would be to keep a cached rotated map Shape in each object, tho that means
// solving the general problem of making sure we're updated any time our
// ultimate map location/size/scale/rotation, etc, changes, which of course
// changes if any of those values change on any ancestor. If we did that, we'd
// also be able to fully cache the _zeroTransform w/out having to recompute it
// for each call just in case. (Which would mean getting rid of this method
// entirely and using the map shape in intersects, etc) Of course, crap, we
// couldn't do all this for links, could we? Tho maybe via special handing in an
// override... tho that would only work for the transform, not the shape, as the
// parent shape is useless to the link. FYI, currently, we only use this
// for doing intersections of links and non-rectangular nodes
// final double[] points = new double[8];
// final double width = zeroRect.getWidth();
// final double height = zeroRect.getHeight();
// // UL
// points[0] = zeroRect.getX();
// points[1] = zeroRect.getY();
// // UR
// points[2] = points[0] + width;
// points[3] = points[1];
// // LL
// points[4] = points[0];
// points[5] = points[1] + height;
// // LR
// points[6] = points[0] + width;
// points[7] = points[1] + height;
// Now that we know the below code can never handle rotation, we also might as
// well toss out using the transform entirely and just use getMapScale /
// getMapX/Y to mod a Rectangle2D.Float directly... Tho then our zoomed rollover
// mod, which is in the transformDown code would stop working for rectangle
// picking & clipping, tho we shouldn't need rect picking for zoomed rollovers,
// (only point picking) and the zoomed rollover always draws no matter what (in
// the MapViewer), so that may be moot, tho would need to fully test to be sure.
// All of the this also applies to transformZeroToMapRect below.
final AffineTransform tx = getZeroTransform();
final double[] points = new double[4];
points[0] = mapRect.getX();
points[1] = mapRect.getY();
points[2] = points[0] + mapRect.getWidth();
points[3] = points[1] + mapRect.getHeight();
try {
tx.inverseTransform(points, 0, points, 0, 2);
} catch (java.awt.geom.NoninvertibleTransformException e) {
Util.printStackTrace(e);
}
zeroRect.setRect(points[0],
points[1],
points[2] - points[0],
points[3] - points[1]
);
return zeroRect;
}
/**
* This will take the given rectangle in local coordinates, and transform it
* into map coordinates. The passed in Rectangle2D will be modified
* and returned.
*/
public Rectangle2D transformZeroToMapRect(Rectangle2D zeroRect) {
return transformZeroToMapRect(zeroRect, zeroRect);
}
/**
* This will take the given zeroRect rectangle in local coordinates, and transform it
* into map coordinates, setting mapRect and returning it. If mapRect is null,
* a new rectangle will be created and returned.
*/
public Rectangle2D transformZeroToMapRect(Rectangle2D zeroRect, Rectangle2D mapRect)
{
final AffineTransform tx = getZeroTransform();
final double[] points = new double[4];
points[0] = zeroRect.getX();
points[1] = zeroRect.getY();
points[2] = points[0] + zeroRect.getWidth();
points[3] = points[1] + zeroRect.getHeight();
tx.transform(points, 0, points, 0, 2);
if (mapRect == null)
mapRect = new Rectangle2D.Float();
mapRect.setRect(points[0],
points[1],
points[2] - points[0],
points[3] - points[1]
);
return mapRect;
// Non-rotating & non-transform using version:
// final double scale = getMapScale();
// // would this be right? scale the x/y first?
// if (scale != 1) {
// rect.x *= scale;
// rect.y *= scale;
// rect.width *= scale;
// rect.height *= scale;
// }
// if (this instanceof LWLink) {
// // todo: eventually rewrite this routine entirely to use the transformations
// // (will need that if ever want to handle rotation, as well as to skip this
// // special case for links).
// rect.x += getParent().getMapX();
// rect.y += getParent().getMapY();
// } else {
// rect.x += getMapX();
// rect.y += getMapY();
// }
}
/**
* Default implementation: checks bounding box
* Subclasses should override and compute via shape.
* INTERSECTIONS always intersect based on map bounds, as opposed to contains, which tests a local point.
*/
public final boolean intersects(Rectangle2D mapRect)
{
return intersectsImpl(mapRect);
// final boolean hit = intersectsImpl(rect);
// //if (DEBUG.PAINT) System.out.println("INTERSECTS " + fmt(rect) + " " + (hit?"YES":"NO ")
// //+ " for " + fmt(getPaintBounds()) + " " + this);
// return hit;
}
/** default impl intersects the render/paint bounds, including any borders (we use this for draw clipping as well as selection) */
protected boolean intersectsImpl(Rectangle2D mapRect) {
//if (DEBUG.CONTAINMENT) System.out.println("INTERSECTS " + Util.fmt(rect));
final Rectangle2D bounds = getPaintBounds();
final boolean hit = mapRect.intersects(bounds);
if (DEBUG.PAINT && DEBUG.PICK) System.out.println("INTERSECTS " + fmt(mapRect) + " " + (hit?"YES":"NO ") + " for " + fmt(bounds) + " of " + this);
//Util.printClassTrace("tufts.vue.LW", "INTERSECTS " + this);
return hit;
}
public boolean requiresPaint(DrawContext dc)
{
return requiresPaintImpl(dc) != null;
}
/** @return true if this component currently requires painting and intersects the master paint region */
protected Object requiresPaintImpl(DrawContext dc)
{
if (dc.skipDraw == this)
return null;
// always draw the focal
if (dc.focal == this)
return "isFocal";
if (isZoomedFocus())
return null;
// if filtered, don't draw, unless has children, in which case
// we need to draw just in case any of the children are NOT filtered.
//if (isHidden() || (isFiltered() && !hasChildren()))
if (!hasDraws())
return null;
if (dc.isClipOptimized()) {
//-----------------------------------------------------------------------------
// Returning true when parent.fullyContainsChildren() is true will prevent a
// ton of intersects calls (and subsequent map-bounds computations involving
// transform fetches and their application to rectangles) when we have lots
// objects that are going to need drawing no matter what (e.g., lots of
// slide icons visible and we're zoomed out), tho it will cause the pixel
// drawing code to be invoked more often that it needs to when zoomed in.
// It's a basic trade-off.
//
// NOT checking this optimizes us for fast painting when zoomed way in on
// sub-components of the map/slides (e.g., during presentations), and that's
// the current chosen priority.
//
// As either method can safely be used (checking or not checking), we allow
// the check, but only if it looks like we're reasonably zoomed-out. Either
// method is okay because this check is just an early way to say something
// requires painting, and it's always okay to paint -- the worse that
// happens is something off screen is painted, and we waste time in the
// graphics pipeline having it clipped. Essentailly, when run, this check
// just lets us skip the intersects call below.
//
// The reason this is meaningful is we only get here if the parent
// has already determined it needs to paint, and if that's the case,
// and it fully contains it's children, if the parent is likely to
// be fully on-screen, we should just go ahead and paint all the children.
if (parent != null && dc.focal != parent && dc.zoom <= 1.0 && parent.fullyContainsChildren()) {
//return "parentIsLikelySlideIcon " + dc.zoom;
return "parentIsLikelySlideIcon";
}
//-----------------------------------------------------------------------------
if (hasEntries() && !(this instanceof LWImage)) {
// HACK: for now, if we have ANY pathway entries, we say we have to draw, so
// that if they're needed, any slide icons will draw (even if the parent
// node is clipped: this is because the slide icons lie outside the
// node). Really, we only need to return true here if we're on any
// pathways that are visible & showing slide icons, and we have at least
// one actual slide. Todo: cache that info so we can check it here
// (such a bit would need to update when any pathway visibility changes,
// or it's show icons bit flips, or our pathway memberships change,
// etc....)
// Also, once we'd determined there were slide icons to draw, we'd
// also want to check each of their bounds to see if they're within
// the master clip rect, tho right now they all scrunch together,
// so that would be a bit of overkill.
// 2009-10-21: too expensive for images as it may force them to load a
// full image representation which takes up tons of memory. Images
// don't display slide icons, so I don't think we ever needed it
// for those anyway.
return "hasEntriesHack";
}
if (intersects(dc.getMasterClipRect()))
return "inClipRegion";
// if (isDrawingSlideIcon())
// return getMapSlideIconBounds().intersects(dc.getMasterClipRect());
// else
return null;
} else {
// Not clip optimized means don't bother to check the master clip to see if
// we need to draw: just always draw everything no matter where it is
// (unless it was hidden, etc). E.g., if we're drawing to generate an
// image, or drawing a zoomed rollover, we already know we just need to draw
// the component no matter what.
// More examples: when drawing raw, always draw everything, don't check
// against the master "map" clip rect, as that's only for drawing map
// elements (e.g., we may be drawing a LWComponent that's a decoration or
// GUI element, like a navigation node, or a master slide background).
return "noClip";
}
}
// /**
// * We divide area around the bounding box into 8 regions -- directly
// * above/below/left/right can compute distance to nearest edge
// * with a single subtract. For the other regions out at the
// * corners, do a distance calculation to the nearest corner.
// * Behaviour undefined if x,y are within component bounds.
// */
// public float distanceToEdgeSq(float x, float y)
// {
// float ex = this.x + getWidth();
// float ey = this.y + getHeight();
// if (x >= this.x && x <= ex) {
// // we're directly above or below this component
// return y < this.y ? this.y - y : y - ey;
// } else if (y >= this.y && y <= ey) {
// // we're directly to the left or right of this component
// return x < this.x ? this.x - x : x - ex;
// } else {
// // This computation only makes sense following the above
// // code -- we already know we must be closest to a corner
// // if we're down here.
// float nearCornerX = x > ex ? ex : this.x;
// float nearCornerY = y > ey ? ey : this.y;
// float dx = nearCornerX - x;
// float dy = nearCornerY - y;
// return dx*dx + dy*dy;
// }
// }
// public Point2D nearestPoint(float x, float y)
// {
// float ex = this.x + getWidth();
// float ey = this.y + getHeight();
// Point2D.Float p = new Point2D.Float(x, y);
// if (x >= this.x && x <= ex) {
// // we're directly above or below this component
// if (y < this.y)
// p.y = this.y;
// else
// p.y = ey;
// } else if (y >= this.y && y <= ey) {
// // we're directly to the left or right of this component
// if (x < this.x)
// p.x = this.x;
// else
// p.x = ex;
// } else {
// // This computation only makes sense following the above
// // code -- we already know we must be closest to a corner
// // if we're down here.
// float nearCornerX = x > ex ? ex : this.x;
// float nearCornerY = y > ey ? ey : this.y;
// p.x = nearCornerX;
// p.y = nearCornerY;
// }
// return p;
// }
// public float distanceToEdge(float x, float y)
// {
// return (float) Math.sqrt(distanceToEdgeSq(x, y));
// }
// /**
// * Return the square of the distance from x,y to the center of
// * this components bounding box.
// */
// public float distanceToCenterSq(float x, float y)
// {
// float cx = getCenterX();
// float cy = getCenterY();
// float dx = cx - x;
// float dy = cy - y;
// return dx*dx + dy*dy;
// }
// public float distanceToCenter(float x, float y)
// {
// return (float) Math.sqrt(distanceToCenterSq(x, y));
// }
// public void drawPathwayDecorations(DrawContext dc)
// {
// if (mPathways == null)
// return;
// if (LWPathway.PathwayAsDots || this instanceof LWLink)
// LWPathway.drawPathwayDot(dc.create(), this);
// if (!LWPathway.PathwayAsDots && isTransparent()) {
// for (LWPathway path : mPathways) {
// //if (!dc.isFocused && path.isDrawn()) {
// if (path.isDrawn()) {
// path.drawPathwayBorder(dc.create(), this);
// }
// }
// }
// }
// /** if this component is selected and we're not printing, draw a selection indicator */
// // todo: drawing of selection should be handled by the MapViewer and/or the currently
// // active tool -- not in the component code
// protected void drawSelectionDecorations(DrawContext dc) {
// if (isSelected() && dc.isInteractive()) {
// LWPathway p = VUE.getActivePathway();
// if (p != null && p.isVisible() && p.getCurrentNode() == this) {
// // SPECIAL CASE:
// // as the current element on the current pathway draws a huge
// // semi-transparent stroke around it, skip drawing our fat
// // transparent selection stroke on this node. So we just
// // do nothing here.
// } else {
// dc.g.setColor(COLOR_HIGHLIGHT);
// dc.g.setStroke(new BasicStroke(getStrokeWidth() + SelectionStrokeWidth));
// transformZero(dc.g);
// dc.g.draw(getZeroShape());
// }
// }
// }
/** @return true if the given x/y (already transformed to our local coordinate space), is within our shape */
public final boolean contains(float x, float y, PickContext pc) {
if (containsImpl(x, y, pc))
return true;
// else if (isDrawingSlideIcon()) {
// if (DEBUG.PICK) out("Checking slide icon bounds " + getSlideIconBounds());
// return getSlideIconBounds().contains(x, y);
// }
else
return false;
}
/** @return 0 means a hit, -1 a completely miss, > 0 means distance (squared), to be sorted out by caller */
protected float pickDistance(float x, float y, PickContext pc) {
return contains(x, y, pc) ? 0 : -1;
}
/**
* Default implementation: checks bounding box, including any stroke width.
* Subclasses should override for more accurate hit detection.
*/
protected boolean containsImpl(float x, float y, PickContext pc)
{
final float stroke = getStrokeWidth() / 2;
return x >= -stroke
&& y >= -stroke
&& x <= getWidth() + stroke
&& y <= getHeight() + stroke;
}
/** For using a node in a non-map context (e.g., as an on-screen button) */
// todo: this is bounding box only: odd shapes will have imperfect hit detection
// also, if we ever add rotation of arbitrary LWComponents, this won't handle it --
// will need need to dump this hack and do all in LWTraversal, or have the
// local LWComponent contains/intersects code adjust for the local transformation
// themselves.
public boolean containsLocalCoord(float x, float y) {
return x >= this.x
&& y >= this.y
&& x <= (this.x+getLocalWidth())
&& y <= (this.y+getLocalHeight());
}
public static final float SlideIconScale = 0.125f;
// private Rectangle2D.Float mSlideIconBounds;
// public Rectangle2D.Float getSlideIconBounds() {
// if (mSlideIconBounds == null)
// mSlideIconBounds = computeSlideIconBounds(new Rectangle2D.Float());
// else if (true || mSlideIconBounds.x == Float.NaN) // need a reshape/reshapeImpl trigger on move/resize to properly re-validate (wait: NaN != NaN !)
// computeSlideIconBounds(mSlideIconBounds);
// return mSlideIconBounds;
// }
// public Rectangle2D.Float getMapSlideIconBounds() {
// Rectangle2D.Float slideIcon = (Rectangle2D.Float) getSlideIconBounds().clone();
// final float scale = getMapScaleF();
// // Compress the local slide icon coords into the node's scale space:
// slideIcon.x *= scale;
// slideIcon.y *= scale;
// // Now make them absolute map coordintes (no longer local):
// slideIcon.x += getMapX();
// slideIcon.y += getMapY();
// // Now scale down size:
// slideIcon.width *= scale;
// slideIcon.height *= scale;
// return slideIcon;
// }
/** @return the local lower right hand corner of the component: for rectangular shapes, this is just [width,height]
* Non-rectangular shapes can override to do something fancier. */
protected Point2D getZeroSouthEastCorner() {
return new Point2D.Float(getWidth(), getHeight());
}
private static final Point2D ZeroNorthWestCorner = new Point2D.Float();
protected Point2D getZeroNorthWestCorner() {
return ZeroNorthWestCorner;
}
// protected Rectangle2D.Float computeSlideIconBounds(Rectangle2D.Float rect)
// {
// // TODO: below should take into account actual slide size...
// final float width = LWSlide.SlideWidth * SlideIconScale;
// final float height = LWSlide.SlideHeight * SlideIconScale;
// Point2D.Float corner = getZeroCorner();
// float xoff = corner.x - 60;
// float yoff = corner.y - 60;
// // If shape is small, try and keep it from overlapping too much (esp the label)
// if (xoff < getWidth() / 2f)
// xoff = getWidth() / 2f;
// if (yoff < getHeight() * 0.75f)
// yoff = getHeight() * 0.75f;
// // This can happen for wierd shapes (e.g., shield)
// if (xoff > corner.x)
// xoff = corner.x;
// if (yoff > corner.y)
// yoff = corner.y;
// rect.setRect(xoff,
// yoff,
// width,
// height);
// return rect;
// }
private Point2D.Float getSlideIconStackLocation()
{
final Point2D corner = getZeroSouthEastCorner();
float xoff = (float) corner.getX() - 60;
float yoff = (float) corner.getY() - 60;
// If shape is small, try and keep it from overlapping too much (esp the label)
if (xoff < getWidth() / 2f)
xoff = getWidth() / 2f;
if (yoff < getHeight() * 0.75f)
yoff = getHeight() * 0.75f;
// todo: can reuse getZeroCorner point2D instead of creating anew...
return new Point2D.Float(xoff, yoff);
}
//protected final Rectangle2D debugZeroRect = new Rectangle2D.Double();
/**
* Intended for use in an LWContainer where the parent has already
* been drawn, and the DrawContext is currently transformed to the
* parent. This performs the final transform for this child and
* transforms it.
*/
public void drawLocal(DrawContext dc)
{
// this will cascade to all children when they draw, combining with their calls to transformDown
transformDownG(dc.g);
final AffineTransform zeroTransform = DEBUG.BOXES ? dc.g.getTransform() : null;
//if (dc.focal == this || dc.isFocused()) // prevents slide icons from appearing in portals
if (dc.focal == this)
drawZero(dc);
else
drawZeroDecorated(dc, true);
if (DEBUG.BOXES)
drawDebugInfo(dc, zeroTransform);
}
private void drawDebugInfo(DrawContext dc, AffineTransform zeroTransform) {
if (this instanceof LWLink)
return;
dc.g.setTransform(zeroTransform);
dc.setAbsoluteStroke(1);
//dc.g.setColor(Color.blue);
//dc.g.draw(debugZeroRect);
// scaling testing -- draw an exactly 8x8 pixel (rendered) box
dc.g.setColor(Color.green);
dc.g.drawRect(0,0,7,7);
// show the center-point to corner intersect line (debug slide icon placement):
dc.g.setColor(Color.red);
//dc.setAbsoluteStroke(1);
dc.g.setStroke(STROKE_ONE);
dc.g.draw(new Line2D.Float(new Point2D.Float(getWidth()/2, getHeight()/2), getZeroSouthEastCorner()));
if (DEBUG.LINK && isSelected() && getLinks().size() > 0) {
final Rectangle2D.Float pureFan = getFanBounds();
final Rectangle2D.Float fan = getCenteredFanBounds();
final float cx = getMapCenterX();
final float cy = getMapCenterY();
final Line2D xaxis = new Line2D.Float(fan.x, cy, fan.x + fan.width, cy);
final Line2D yaxis = new Line2D.Float(cx, fan.y, cx, fan.y + fan.height);
dc.setMapDrawing();
dc.setAbsoluteStroke(4);
//dc.g.setColor(getRenderFillColor(dc));
dc.g.setColor(Color.blue);
dc.g.draw(pureFan);
dc.setAbsoluteStroke(2);
dc.g.setColor(Color.red);
dc.g.draw(fan);
dc.g.draw(xaxis);
dc.g.draw(yaxis);
}
}
/**
*
* This is NOT the method used to draw a component during routine drawing of the
* entire map (unless this is the map itself). This is for directly forcing the
* drawing or redrawing a single component at it's proper map location. The passed
* in DrawContext gc is expected to be transformed for drawing the top-level map
* (minimally transformed). If you are going to use the passed in DrawContext after
* this call for other map drawing operations, be sure to pass in dc.create() from
* the caller, as this call will leave the DrwaContext it in a generally undefined state
* (e.g., probably rooted at the node).
*
*/
public void draw(DrawContext dc) {
// if (dc.isPrintQuality()) {
// dc.setClipOptimized(false); // ensure all children draw even if not inside clip
// // in interactive presentation mode, the above causes repaint-region's to
// // cause all images to draw in a slide focal, which can cause them to LOSE
// // resolution under low memory conditions.
// }
transformZero(dc.g);
if (dc.focal == this) {
drawZero(dc);
} else {
drawZeroDecorated(dc, false);
// if (isZoomedFocus()) {
// // include any slide icons
// drawDecorated(dc);
// } else {
// if (dc.drawPathways())
// drawPathwayDecorations(dc);
// drawRaw(dc);
// }
}
}
public final void drawZero(DrawContext dc)
{
final AffineTransform zeroTransform = DEBUG.PDF ? dc.g.getTransform() : null;
dc.checkComposite(this);
if (DEBUG.Enabled) dc.recordDebug(this);
try {
if (!isTopLevel()) { // e.g., isn't a Layer, which is never selected
// TODO: this should be a flag set up in the DrawContext
if(VUE.getInteractionToolsPanel() != null ){
final double alpha = VUE.getInteractionToolsPanel().getAlpha();
if (alpha != 1 && !selectedOrParent())
dc.setAlpha(alpha); // fade nodes not in selection
}
}
drawImpl(dc);
} catch (RuntimeException e) {
Log.error("drawImpl failed: " + e);
try {
dc.setAlpha(0.5);
dc.g.setColor(Color.red);
dc.g.fill(getZeroShape());
} catch (Throwable t) {
Util.printStackTrace(t);
} finally {
throw e;
}
}
if (isDeleted()) {
// debug
dc.setAlpha(0.5);
dc.g.setColor(Color.yellow);
dc.g.fill(getZeroShape());
}
if (DEBUG.PDF && DEBUG.META && this instanceof LWLink == false) {
dc = dc.create();
dc.g.setTransform(zeroTransform);
dc.g.setColor(Color.blue);
dc.g.setFont(VueConstants.FixedSmallFont);
dc.setAbsoluteScale(1);
final Color c1 = getFillColor();
final Color c2 = getRenderFillColor(dc);
dc.g.drawString(fmt(c1), 0, 10);
if (c1 == null || !c1.equals(c2))
dc.g.drawString(fmt(c2), 0, 20);
}
}
// public void drawFit(java.awt.Graphics g, int xoff, int yoff) {
// //drawFit(dc, dc.getMasterClipRect(), borderGap);
// drawFit(new DrawContext(g, this), 0);
// }
/** fit and center us into the total clip bounds of the given dc -- border gap pixels will multiplied by final scale value */
public void drawFit(DrawContext dc, int borderGap) {
drawFit(dc, dc.getMasterClipRect(), borderGap);
}
/**
* fit and center us into the given frame
* @param borderGap: if positive, a fixed amount of pixels, if NEGATIVE, the % of viewport size to leave as a margin
*/
public void drawFit(DrawContext dc, Rectangle2D frame, int borderGap)
{
final Point2D.Float offset = new Point2D.Float();
final Size size = new Size(frame);
final double zoom = ZoomTool.computeZoomFit(size,
borderGap,
addStrokeToBounds(getZeroBounds(), 0),
offset);
if (DEBUG.PDF) out("drawFit into " + fmt(frame) + " zoom " + zoom);
dc.g.translate(-offset.x + frame.getX(),
-offset.y + frame.getY());
dc.g.scale(zoom, zoom);
dc.setClipOptimized(false);
drawZero(dc);
}
//private static final double PathwayOnTopZoomThreshold = 1.5;
public static final double PathwayOnTopZoomThreshold = 3;
/**
* Draw any needed pathway decorations and related slide icons,
* before/after calling drawZero, depending on desired impl.
*/
protected final void drawZeroDecorated(DrawContext dc, boolean drawSlides)
{
if (dc.drawPathways() && mPathways != null) {
LWPathway.decorateUnder(this, dc);
if (dc.zoom > PathwayOnTopZoomThreshold) {
// force the over decorations to be under
LWPathway.decorateOver(this, dc);
drawZero(dc);
} else {
drawZero(dc);
LWPathway.decorateOver(this, dc);
}
} else {
drawZero(dc);
}
// see VUE-896 (and VUE-892) only show slide icon if node is not filtered
if (drawSlides && mEntries != null && !isFiltered())
drawSlideIconStack(dc);
//else
//farthestVisibleSlideCorner = null;
}
// /** If there's a pathway entry we want to be showing, return it, otherwise, null */
// LWPathway.Entry getEntryToDisplay()
// {
// LWPathway path = VUE.getActivePathway();
// if (!inPathway(path)) {
// if (mPathways != null && mPathways.size() > 0)
// path = mPathways.get(0); // show the first pathway it's in if it's not in the active pathway
// else
// path = null;
// }
// if (path != null && path.isShowingSlides()) {
// final LWPathway.Entry entry = path.getCurrentEntry();
// // This is just in case the node is in the pathway more than once: if it is,
// // and the current entry is for this node, use that, otherwise, just
// // use the first entry for the the node.
// if (entry != null && entry.node == this)
// return entry;
// else
// return path.getEntry(path.firstIndexOf(this));
// }
// return null;
// }
// public boolean isDrawingSlideIcon() {
// final LWPathway.Entry entry = getEntryToDisplay();
// return entry != null && !entry.isMapView;
// }
private Point2D.Float farthestVisibleSlideCorner;
private void recordCorner(Point2D.Float p) {
//if (p == null) return; // try always leaving last corner for now
if (DEBUG.WORK) out("corner=" + p);
farthestVisibleSlideCorner = p;
}
/** @return a slide to be drawn last, or null if none in particular */
// TODO: need to do this as part of layout: need to trigger layout if any pathway
// visibility or membership changes. Currently, the paint bounds falls behind as
// farthestVisibleCorner doesn't update till draw time... We special case a call to
// this during the init (restore) layout, so at least auto-fit at startup works, but
// if we ever hava a viewer that's implementing a constant auto-fit feature, it will
// fall behind until we handle this in proper model/view split fashion.
private final void layoutSlideIcons(DrawContext dc) {
if (mEntries == null)
return;
final Point2D.Float corner = getSlideIconStackLocation();
float xoff = corner.x;
float yoff = corner.y;
// if (false && dc != null && dc.isPresenting()) {
// // if presenting, let the position the active pathway slide as the last slide in the stack
// for (LWSlide slide : seenSlideIcons(dc)) {
// slide.takeLocation(xoff, yoff);
// yoff += slide.getLocalHeight() / 6;
// xoff += slide.getLocalWidth() / 6;
// }
// } else {
// if NOT presenting, leave the slides arranged in the order
// of the pathway list (TODO: entries order isn't synced with this...)
LWSlide lastSlide = null;
for (LWPathway.Entry e : mEntries) {
if (e.hasVisibleSlide()) {
final LWSlide slide = e.getSlide();
lastSlide = slide;
// TODO BUG: during a presentation, if you use 'p' to change to exclusive
// pathway display mode and back, it actually results in the movement of the
// slide icons -- so unless the slide you're on happens to be the first slide
// icon, it will move when you do this, and the viewer isn't catching this, and
// the slide moves up and to the left in the middle of the presentation. The
// real fix for this involves the complete reworking of the MapViewer to just
// be a Focal/Tree viewer, which can truly throw out all map coordinates, and
// just deal with wherever it's rooted in the hierachy. We're probably 2/3 of
// the way there now. Hopefully we'll get there someday... SMF 2007-11-05
// so if is the focal somewhere, it can know to zoom fit, tho ideally
// the viewer would just ignore the location on focals...
// slide.setLocation(xoff, yoff); // No good: if slide's parent moves, the slide moves..
slide.takeLocation(xoff, yoff);
final float scaledSlideWidth = slide.getLocalWidth();
final float scaledSlideHeight = slide.getLocalHeight();
yoff += scaledSlideWidth / 6;
xoff += scaledSlideHeight / 6;
}
}
if (lastSlide != null) {
corner.x = lastSlide.getMapX() + lastSlide.getLocalWidth();
corner.y = lastSlide.getMapY() + lastSlide.getLocalHeight();
recordCorner(corner);
//out("far corner: " + Util.fmt(corner));
} else
recordCorner(null);
// Now just in case, layout all the non-visible ones after the visible, in case
// they get manually selected via the pathway panel and temporarily shown
// (only one can be shown at a time, so they can all occupy the last slot)
for (LWPathway.Entry e : mEntries) {
if (!e.pathway.isShowingSlides() && e.canProvideSlide()) {
e.getSlide().takeLocation(xoff, yoff);
}
}
//}
}
private void drawSlideIconStack(final DrawContext dc)
{
layoutSlideIcons(dc);
dc.setBackgroundFill(null); // always make sure the slide icons fill
for (LWSlide slide : seenSlideIcons(dc)) {
if (dc.skipDraw != slide) {
drawSlideIcon(dc.push(), slide);
dc.pop();
}
}
}
protected static final BasicStroke SlideIconPathwayStroke =
new BasicStroke((float) (LWPathway.PathBorderStrokeWidth / SlideIconScale),
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
private void drawSlideIcon(final DrawContext dc, final LWSlide slide)
{
slide.transformDownG(dc.g);
// final boolean drewBorder;
// //if (dc.isPresenting() || slide.isSelected()) {
// if (dc.isPresenting() || slide.getPathwayEntry() == VUE.getActiveEntry()) {
// // every slide icon should be a slide with an entry...
// dc.g.setColor(slide.getPathwayEntry().pathway.getColor());
// //dc.g.setColor(Color.red);
// dc.g.setStroke(SlideIconPathwayStroke);
// dc.g.draw(slide.getZeroShape());
// drewBorder = true;
// } else {
// drewBorder = false;
// }
// final AffineTransform zeroTransform = dc.g.getTransform();
// final Shape curClip = dc.g.getClip();
// dc.g.clip(slide.getZeroShape());
slide.drawZero(dc);
// if (!drewBorder && !dc.isAnimating()) {
// dc.g.setClip(curClip); // TODO: this is clearing the underlying clip and allowing the border to draw over the scroll bars, etc!
// // Generic non-presentation unselected slide icon: draw a gray border
// //dc.g.setColor(slide.getRenderFillColor(dc).brighter());
// dc.g.setTransform(zeroTransform);
// dc.g.setColor(Color.darkGray);
// dc.g.setStroke(STROKE_FIVE);
// dc.g.draw(slide.getZeroShape());
// }
}
/*
protected final void drawDecorated(DrawContext dc)
{
final LWPathway.Entry entry = getEntryToDisplay();
//final boolean drawSlide = (entry != null);
final boolean drawSlide = (entry != null && !entry.isMapView);
if (dc.drawPathways() && dc.focal != this)
drawPathwayDecorations(dc);
if (drawSlide) {
drawZero(dc);
final LWSlide slide = entry.getSlide();
//double slideX = getCenterX() - (slide.getWidth()*slideScale) / 2;
//double slideY = getCenterY() - (slide.getHeight()*slideScale) / 2;
//dc.g.translate(slideX, slideY);
Rectangle2D.Float slideFrame = getSlideIconBounds();
//slide.setLocation(slideFrame.x, slideFrame.y);
dc.setClipOptimized(false);
dc.g.translate(slideFrame.x, slideFrame.y);
dc.g.scale(SlideIconScale, SlideIconScale);
// A hack so that when LWLinks (hasAbsoluteMapLocation) pop to map drawing, they
// don't pop up beyond this point.
//dc.mapTransform = dc.g.getTransform();
//dc.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.9f));
//entry.pathway.getMasterSlide().drawImpl(dc);
slide.drawImpl(dc);
//Rectangle2D border = slideFrame;
// todo: move to LWSlide.drawImpl:
Rectangle2D border = slide.getBounds();
final Color slideFill = slide.getRenderFillColor(dc);
final Color iconBorder;
// todo: create a contrastColor, which node icon border's can also use
if (brightness(slideFill) == 0)
iconBorder = Color.gray;
else if (brightness(slideFill) > 0.5)
iconBorder = slideFill.darker();
else
iconBorder = slideFill.brighter();
//out("slideFillr: " + slideFill);
//out("iconBorder: " + iconBorder);
dc.g.setColor(iconBorder);
dc.g.setStroke(VueConstants.STROKE_SEVEN);
dc.g.draw(border);
} else {
//if (entry != null && !dc.isFocused) {
if (entry != null) {
// if we had an entry, but it was a map-view slide, do something to make it look slide-like
dc.g.setColor(entry.pathway.getMasterSlide().getFillColor());
if (entry.node instanceof LWGroup) {
if (!dc.isPresenting())
dc.g.fill(entry.node.getZeroBounds());
} else if (dc.focal != this && entry.node.isTranslucent()) {
Area toFill = new Area(entry.node.getZeroBounds());
toFill.subtract(new Area(entry.node.getZeroShape()));
dc.g.fill(toFill);
}
}
drawZero(dc);
}
}
*/
/** default impl: does nothing -- meant to be overriden */
protected void drawImpl(DrawContext dc) {}
/** default impl: does nothing -- meant to be overriden -- meant to potentially cache all children */
public void preCacheContent() {}
protected void preCacheImpl() {}
protected static void preCacheDescendents(LWComponent focal) {
if (focal == null)
return;
//Log.debug("PRE CACHE FOCAL " + focal);
for (LWComponent c : focal.getAllDescendents()) {
//Log.debug("PRE-CACHE-CHILD " + c);
c.preCacheImpl();
}
}
protected LWChangeSupport getChangeSupport() {
return mChangeSupport;
}
public synchronized void addLWCListener(Listener listener) {
mChangeSupport.addListener(listener, null);
}
public synchronized void addLWCListener(Listener listener, LWComponent.Key singleEventKey) {
mChangeSupport.addListener(listener, singleEventKey);
}
/** @param eventMask is a string constant (from LWKey) or an array of such. If one
of these non-null values, only events matching those keys will be delievered */
public synchronized void addLWCListener(Listener listener, Object... eventsDesired) {
mChangeSupport.addListener(listener, eventsDesired);
}
public synchronized void removeLWCListener(Listener listener) {
mChangeSupport.removeListener(listener);
}
/** convenince method for remove a (possible) old listener, and attaching a (possible) new listener */
public static void swapLWCListener(Listener listener, LWComponent oldSource, LWComponent newSource) {
if (oldSource != null)
oldSource.removeLWCListener(listener);
if (newSource != null)
newSource.addLWCListener(listener);
}
public synchronized void removeAllLWCListeners() {
mChangeSupport.removeAllListeners();
}
private boolean isStyling(Key key) {
return supportsProperty(key)
//&& (key.isStyleProperty() || key == LWKey.Label);
&& (key.isStyleProperty() || (key == LWKey.Label && hasFlag(Flag.DATA_STYLE)));
}
protected synchronized void notifyLWCListeners(LWCEvent e)
{
if (isDeleted() && !permitZombieEvent(e)) {
// note: this test shortcuts much more detailed diagnostics in LWChangeSupport
// for tracking zombie events -- comment out for advanced debugging
if (DEBUG.Enabled) Log.debug(this + " ignoring: " + e);
return;
}
if (hasFlag(Flag.EVENT_SILENT))
return;
// //if (e.key.isSignal || e.key == LWKey.Location && e.source == this) {
// if (e.key == LWKey.UserActionCompleted || e.key == LWKey.Location && e.source == this) {
// // only keep if the location event is on us:
// // if this is our child that moved, obviously
// // clear the cache (we look different)
// //out("*** KEEPING IMAGE CACHE ***");
// ; // keep the cached image
// } else {
// //out("*** CLEARING IMAGE CACHE");
// //mImageBuffer = null;
// }
mChangeSupport.notifyListeners(this, e);
if (e.component == this && e.key instanceof Key) {
// if parent is null, we're still initializing
final Key key = (Key) e.key;
//Log.debug("Checking for style update " + e + "\n\tisStyle: " + isStyle() + "\n\tisStyling: " + isStyling(key));
if (isStyle() && isStyling(key))
updateStyleWatchers(key, e);
// sync sources not in use: never do this 2007-11-30 SMF
//if (key.type == KeyType.DATA)
// syncUpdate(key);
}
// if (isStyle() && getParent() == null)
// ; // ignore events from non-embedded style objects (e.g., EditorManager constructs)
// else
// mChangeSupport.notifyListeners(this, e);
// if (getParent() != null && e.component == this && e.key instanceof Key) {
// // if parent is null, we're still initializing
// final Key key = (Key) e.key;
// if (isStyle() && key.isStyleProperty)
// updateStyleWatchers(key, e);
// // sync sources not in use: never do this 2007-11-30 SMF
// //if (key.type == KeyType.DATA)
// // syncUpdate(key);
// }
// labels need own call to this due to TextBox use of setLabel0
}
/** @return false -- subclasses can override */
protected boolean permitZombieEvent(LWCEvent e) {
return hasFlag(Flag.INTERNAL);
}
/** Copy the value for the given key either back to our sync source, or to our sync clients */
private boolean syncUnderway = false;
private void syncUpdate(Key key) {
if (syncUnderway)
return;
syncUnderway = true;
try {
doSyncUpdate(key);
} finally {
syncUnderway = false;
}
}
protected void doSyncUpdate(Key key) {
// currently we only allow one or the other: you can be a source, or a client
// this is all we need for now (a node can be synced to nodes on multiple
// slides on different pathways, but a node in a slide can only refer
// back to one source)
if (mSyncSource != null) {
Log.debug("[" + key + "] UPDATING SYNC SOURCE: " + this + " -> " + mSyncSource);
if (!mSyncSource.isDeleted())
key.copyValue(this, mSyncSource);
} else if (mSyncClients != null && !mSyncClients.isEmpty()) {
for (LWComponent c : mSyncClients) {
Log.debug("[" + key + "] UPDATING SYNC CLIENT: " + this + " -> " + c);
//Util.printStackTrace("SYNCTRACE " + this);
if (!c.isDeleted())
key.copyValue(this, c);
}
}
}
/** If the event is a change for a style property, apply the change to all
LWComponents that refer to us as their style parent */
protected void updateStyleWatchers(Key key, LWCEvent e)
{
if (!isStyling(key) || mXMLRestoreUnderway) {
// nothing to do if this isn't a style property that's changing
return;
}
// Now we know a styled property is changing. Since they Key itself
// knows how to get/set/copy values, we can now just find all the
// components "listening" to this style (pointing to it), and copy over
// the value that just changed on the style object.
if (DEBUG.Enabled) out("\nSTYLE OBJECT UPDATING STYLED CHILDREN with " + key + "; value " + Util.tags(e.getOldValue()));
//final LWPathway path = ((MasterSlide)getParent()).mOwner;
// We can traverse all objects in the system, looking for folks who
// point to us. But once slides are owned by the pathway, we'll have a
// list of all slides here from the pathway, and we can just traverse
// those and check for updates amongst the children, as we happen
// to know that this style object only applies to slides
// (as opposed to ontology style objects)
// todo: this not a fast way to traverse & find what we need to change...
for (LWComponent dest : findPotentialStyleWatchers()) {
// we should never be point back to ourself, but we check just in case
if (dest.mParentStyle == this && dest.supportsProperty(key) && dest != this) {
// Only copy over the style value if was previously set to our existing style value
try {
if (key.isStyleProperty()) {
if (key.valueEquals(dest, e.getOldValue())) {
key.copyValue(this, dest);
} else if (DEBUG.STYLE) {
System.err.println(" SKIP-USER-SET: " + dest
+ "; target has user-override value: " + Util.tags(key.getValue(dest)));
}
} else {
if (DEBUG.STYLE) Log.debug("DATA-STYLE COPY " + key + " -> " + dest);
key.copyValue(this, dest);
}
} catch (Throwable t) {
tufts.Util.printStackTrace(t, "Failed to copy value from " + e + " old=" + e.getOldValue());
}
}
}
}
private Collection<LWComponent> findPotentialStyleWatchers() {
final LWMap map;
if (getParent() == null)
//map = getClientData(LWMap.class, "styledMap");
map = VUE.getActiveMap(); // a bit of a hack
else
map = getMap();
return map.getAllDescendents(ChildKind.ANY);
//return getMap().getAllDescendents(ChildKind.ANY);
}
/**
* A third party can ask this object to raise an event
* on behalf of the source.
*/
public void notify(Object source, String what)
{
notifyLWCListeners(new LWCEvent(source, this, what));
}
void notifyProxy(LWCEvent e) {
notifyLWCListeners(e);
}
// /** This generates an event with NO COMPONENT IN IT: we just want access to the model hierarchy at
// * this point, but the component itself is not interesting here.
// */
// void notifyProxy(Object source, String what)
// {
// notifyProxy(new LWCEvent(source, null, what));
// }
protected void notify(String what, LWComponent contents)
{
if (alive())
notifyLWCListeners(new LWCEvent(this, contents, what));
}
protected void notify(String what, Object oldValue)
{
// // TODO PERFORMANCE: have EVENT_SILENT always be true for initializing nodes -- this
// // should speed things up when creating hundreds/thousands(!) of nodes, including
// // during map restores -- we can skip creating an event that will never be delievered
// // for ever property set that happens
// if (hasFlag(Flag.EVENT_SILENT))
// return;
if (alive())
notifyLWCListeners(new LWCEvent(this, this, what, oldValue));
}
/** same as notify(String, Object), but will do notification even if the LWComponent isn't "alive" yet */
protected void notifyForce(String what, Object oldValue)
{
notifyLWCListeners(new LWCEvent(this, this, what, oldValue));
}
protected void notify(Key key, Object oldValue)
{
if (alive())
notifyLWCListeners(new LWCEvent(this, this, key, oldValue));
}
protected void notify(Key key, boolean oldValue)
{
if (alive())
notify(key, oldValue ? Boolean.TRUE : Boolean.FALSE);
}
protected void notify(String what)
{
// todo: we still need both src & component? (this,this)
notifyLWCListeners(new LWCEvent(this, this, what, LWCEvent.NO_OLD_VALUE));
}
/**a notify with an array of components
added by Daisuke Fujiwara
*/
protected void notify(String what, List<LWComponent> componentList)
{
notifyLWCListeners(new LWCEvent(this, componentList, what));
}
/**
* Delete this single component: equivalent to a user-delete action. This component
* will be removed from it's parent, and disconnected from all relationships in the
* model.
*/
public void delete() {
if (!isDeleted())
getParent().deleteChildPermanently(this);
else
Log.debug("attempt to delete already deleted: " + this);
}
/**
* Do final cleanup needed now that this LWComponent has
* been removed from the model. Calling this on an already
* deleted LWComponent has no effect. This will raise
* an LWKey.Deleting event before the component is actually deleted.
*/
protected void removeFromModel()
{
if (isDeleted()) {
if (DEBUG.PARENTING||DEBUG.EVENTS) out(this + " removeFromModel(lwc): ignoring (already removed)");
return;
}
if (DEBUG.PARENTING||DEBUG.EVENTS) out(this + " removeFromModel(lwc)");
//throw new IllegalStateException(this + ": attempt to delete already deleted");
setFlag(Flag.DELETING);
notify(LWKey.Deleting);
prepareToRemoveFromModel();
removeAllLWCListeners();
disconnectFromLinks(); // if any of the links themseleves are being deleted, we don't actually need to disconnect
clearFlag(Flag.DELETING);
setDeleted(true);
}
/**
* For subclasses to override that need to do cleanup
* activity before the the default LWComponent removeFromModel
* cleanup runs.
*/
protected void prepareToRemoveFromModel() { }
/** undelete -- called just before the UndoManager calls setParent */
protected void restoreToModel()
{
// TODO: UNDELETING flag may be functionally vestigal at this point.
// Also, would want to split this in to a final restoreToModel and
// an overridable restoreToModelImpl wrapped by the calls that
// force having the UNDELETING bit set, if we really want to rely on it.
setFlag(Flag.UNDELETING);
if (DEBUG.PARENTING||DEBUG.EVENTS) out("restoreToModel: " + this);
if (!isDeleted()) if (DEBUG.Enabled) out("FYI: already restored");
// we need this only in case node-icon preferences have changed since this object left the
// model -- otherwise nothing could have happened to this component to change it. Any
// possible size/location events that could happen as a result will be ignored as this
// component doesn't have it's parent set yet.
layout();
setDeleted(false);
clearFlag(Flag.UNDELETING);
}
public boolean isDeleted() {
return hasFlag(Flag.DELETED);
}
private void setDeleted(boolean deleted) {
if (deleted) {
//mHideBits |= HideCause.DELETED.bit; // direct set: don't trigger notify
setFlag(Flag.DELETED);
if (DEBUG.PARENTING||DEBUG.UNDO||DEBUG.EVENTS)
if (parent != null) out("parent not yet null in setDeleted true (ok for undo of creates)");
this.parent = null;
} else
clearFlag(Flag.DELETED);
}
private void disconnectFromLinks()
{
// iterate through copy of the list, as it may be modified concurrently during removals
if (mLinks != null) {
for (LWLink link : mLinks.toArray(new LWLink[mLinks.size()]))
link.disconnectFrom(this);
}
clearHidden(HideCause.PRUNE);
}
// public void setSelected(boolean selected) {
// this.selected = selected;
// }
// public final boolean isSelected() {
// return this.selected;
// }
public void setSelected(boolean selected) {
setFlag(Flag.SELECTED, selected);
}
public final boolean isSelected() {
return hasFlag(Flag.SELECTED);
}
protected boolean selectedOrParent() {
return parent == null ? isSelected() : (isSelected() || parent.selectedOrParent());
//return parent == null ? isSelected() : (parent.selectedOrParent() | isSelected());
}
public final boolean isAncestorSelected() {
return parent == null ? false : parent.selectedOrParent();
}
public final boolean isPruned() {
//return hasState(State.PRUNED); // for undo attempts
return hasFlag(Flag.PRUNED);
}
public final void setPruned(boolean pruned) {
//setState(State.PRUNED, pruned); // for undo attempts
setFlag(Flag.PRUNED, pruned);
}
public static final Key KEY_State =
new Key<LWComponent,Integer>("state") {
@Override public void setValue(LWComponent c, Integer state) { c.mState = state; } // for undo
@Override public Integer getValue(LWComponent c) { return c.mState; } // for undo/debug
@Override public String getStringValue(LWComponent c) { return Integer.toHexString(c.mState); }
// arch: would be nice if all the stuff for dealing with bitfields was handled in a single
// BooleanKey which could be used for multiple bit fields.
};
public void setState(State s) {
setState(s, true);
}
public void setState(State s, boolean on) {
final int old = mState;
if (on)
mState |= s.bit;
else
mState &= ~s.bit;
// Problems getting below to fully work:
// (1) map not repainting when undoing a clear all pruning (should be easy)
// (2) LWLink internal head/tail prune states also need to be synced -- would need to make these
// state bits as well for being able to undo individual mouse-click prunes.
// if (s == State.PRUNED) {
// // this is a hack, but could make this work for undo:
// if (on) {
// if (LWLink.isPruningEnabled()) {
// setHidden(HideCause.PRUNE, true);
// }
// } else {
// setHidden(HideCause.PRUNE, false);
// }
// }
if (mState != old)
notify(KEY_State, Integer.valueOf(old));
}
public boolean hasState(State s) {
return (mState & s.bit) != 0;
}
public void setFlag(Flag flag) {
if (DEBUG.DATA) out("setFlag " + flag);
mFlags |= flag.bit;
}
public void clearFlag(Flag flag) {
if (DEBUG.DATA) out("clearFlag " + flag);
mFlags &= ~flag.bit;
}
public void setFlag(Flag flag, boolean set) {
if (set)
setFlag(flag);
else
clearFlag(flag);
}
public boolean hasFlag(Flag flag) {
return (mFlags & flag.bit) != 0;
}
public boolean hasAnyFlag(int bits) {
return (mFlags & bits) != 0;
}
public void setLocked(boolean locked) {
if (hasFlag(Flag.LOCKED) != locked) {
setFlag(Flag.LOCKED, locked);
notify("locked");
}
}
public final boolean isLocked() {
return hasFlag(Flag.LOCKED);
}
public void setCollapsed(boolean collapsed) {
// Default does nothing: only LWNode impl currebntly allows a collapsed state.
// Move up the LWNode impl (which is generic) if we want more
// than just LWNode's to support a collapsed state.
}
public boolean isCollapsed() {
if (COLLAPSE_IS_GLOBAL)
return false; // LWNode overrides
//return isGlobalCollapsed;
else
return hasFlag(Flag.COLLAPSED);
}
public boolean isAncestorCollapsed()
{
// if (COLLAPSE_IS_GLOBAL) {
// return isGlobalCollapsed;
// }
if (parent != null) {
if (parent.isCollapsed())
return true;
else
return parent.isAncestorCollapsed();
} else
return false;
}
public Boolean getXMLlocked() {
return isLocked() ? Boolean.TRUE : null;
}
public void setXMLlocked(Boolean b) {
setLocked(b);
}
// /** debug -- names of set HideBits */
// String getDescriptionOfSetBits() {
// StringBuffer buf = new StringBuffer();
// for (HideCause reason : HideCause.values()) {
// if (isHidden(reason)) {
// if (buf.length() > 0)
// buf.append(',');
// buf.append(reason);
// }
// }
// return buf.toString();
// }
String getDescriptionOfSetBits() {
String s = "";
if (mHideBits != 0)
s += getDescriptionOfSetBits(HideCause.class, mHideBits);
if (mFlags != 0) {
if (s.length() > 0)
s += "; ";
s += getDescriptionOfSetBits(Flag.class, mFlags);
}
return s;
}
String getDescriptionOfSetBits(Class enumType, long bits) {
final StringBuilder buf = new StringBuilder();
//buf.append(enumType.getSimpleName());
buf.append(enumType.getSimpleName().substring(0,2));
buf.append('(');
boolean first = true;
for (Object eValue : enumType.getEnumConstants()) {
final Enum e = (Enum) eValue;
if ((bits & (1<<e.ordinal())) != 0) {
if (!first)
buf.append(',');
buf.append(eValue);
//buf.append(':');buf.append(e.ordinal());
first = false;
}
}
buf.append(')');
return buf.toString();
}
public void setVisible(boolean visible) {
setHidden(HideCause.DEFAULT, !visible);
}
public void setHidden(HideCause cause, boolean hide) {
if (hide)
setHidden(cause);
else
clearHidden(cause);
}
public void setHidden(HideCause cause) {
if (DEBUG.EVENTS) out("setHidden " + cause);
setHideBits(mHideBits | cause.bit);
}
public void clearHidden(HideCause cause) {
//Log.debug(this, new Throwable("clearHidden"));
if (DEBUG.EVENTS) out("clrHidden " + cause);
setHideBits(mHideBits & ~cause.bit);
}
private void setHideBits(int bits) {
final boolean wasHidden = isHidden();
mHideBits = bits;
if (wasHidden != isHidden())
notify(LWKey.Hidden);
//notify(LWKey.Hidden, wasHidden); // if we need it to be undoable
}
/**
* @return true if this component has been hidden. Note that this
* is different from isFiltered. All children of a hidden component
* are also hidden, but not all children of a filtered component
* are hidden.
*/
public final boolean isHidden() {
return !isVisible();
}
public boolean isHidden(HideCause cause) {
return (mHideBits & cause.bit) != 0;
}
public boolean isVisible() {
return mHideBits == 0;
}
/** persist with value true only if HideCause.DEFAULT is set */
public Boolean getXMLhidden() {
return isHidden(HideCause.DEFAULT) ? Boolean.TRUE : null;
}
public void setXMLhidden(Boolean b) {
setVisible(!b.booleanValue());
}
/** persist with a true value only if HideCause.PRUNE is set */
public Boolean getXMLpruned() {
// note: could store this as two bits on the links instead and reconsitute
// from that as opposed to saving on every node
return isPruned() ? Boolean.TRUE : null;
}
public void setXMLpruned(Boolean b) {
// note: should normally only be called if b is true,
// as when false it shouldn't be persisted at all
setPruned(b.booleanValue());
}
/** @deprecated -- use hasDraws() */
public final boolean isDrawn() {
return hasDraws();
}
/** @return true if ths component is going to be painting itself (independent of any children may do so) */
public boolean isPainted() {
return isVisible() && !isFiltered();
}
/** @return true if this node may have any drawing to do: (e.g., itself or children)
* Note that a return of true does not guarantee that we will draw anything,
* but if it returns false it does guarantee that nothing needs drawing */
public boolean hasDraws() {
if (isFiltered())
return hasChildren();
else
return isVisible();
}
protected boolean updatingLinks() {
return !isZoomedFocus() || DEBUG.VIEWER;
}
public void mouseEntered(MapMouseEvent e)
{
if (DEBUG.ROLLOVER) System.out.println("MouseEntered: " + this);
//e.getViewer().setIndicated(this);
mouseOver(e);
}
public void mouseMoved(MapMouseEvent e)
{
//System.out.println("MouseMoved " + this);
mouseOver(e);
}
public void mouseOver(MapMouseEvent e)
{
//System.out.println("MouseOver " + this);
}
public void mouseExited(MapMouseEvent e)
{
if (DEBUG.ROLLOVER) System.out.println(" MouseExited: " + this);
//e.getViewer().clearIndicated();
}
/** pre-digested single-click
* @return true if you do anything with it, otherwise
* the viewer can/will provide default action.
*/
public boolean handleSingleClick(MapMouseEvent e)
{
return false;
}
/** pre-digested double-click
* @return true if you do anything with it, otherwise
* the viewer can/will provide default action.
* Default action: if we have a resource, launch
* it in a browser, otherwise, do nothing.
*/
public boolean handleDoubleClick(MapMouseEvent e)
{
if (hasResource()) {
out("Displaying content for: " + getResource());
getResource().displayContent();
return true;
}
else if (this instanceof LWGroup) // todo: in override
{
//} else if (this instanceof LWSlide || this instanceof LWGroup || this instanceof LWPortal)
// MapViewer "null remote focal" code would need fixing to enable selection if a portal is the focal
// (the selected objects are not children of the focal, so they don't look like we should be seeing them)
VUE.getReturnToMapButton().setVisible(true);
VUE.depthSelectionControl.setVisible(false);
return doZoomingDoubleClick(e);
} else
return false;
}
public static final boolean SwapFocalOnSlideZoom = true;
private static final boolean AnimateOnZoom = true;
protected boolean doZoomingDoubleClick(MapMouseEvent e)
{
// System.out.println("zooming double click");
final MapViewer viewer = e.getViewer();
if (viewer.getFocal() == this) {
viewer.popFocal(MapViewer.POP_TO_TOP, MapViewer.ANIMATE);
return true;
//return false;
}
VUE.getActiveMap().setTempBounds(VUE.getActiveViewer().getVisibleMapBounds());
final Rectangle2D viewerBounds = viewer.getVisibleMapBounds();
final Rectangle2D mapBounds = getMapBounds();
final Rectangle2D overlap = viewerBounds.createIntersection(mapBounds);
final double overlapArea = overlap.getWidth() * overlap.getHeight();
//final double viewerArea = viewerBounds.getWidth() * viewerBounds.getHeight();
final double nodeArea = mapBounds.getWidth() * mapBounds.getHeight();
final boolean clipped = overlapArea < nodeArea;
final double overlapWidth = mapBounds.getWidth() / viewerBounds.getWidth();
final double overlapHeight = mapBounds.getHeight() / viewerBounds.getHeight();
final boolean focusNode; // otherwise, re-focus map
// Note: this code is way more complicated than we're making use of right now --
// we always fully load objects (slides) as the focal when we zoom to them.
// This code permitted double-clicking through a slide-icon stack, where we'd
// zoom to the slide icon, but retain the map focal. The overlap herustics here
// determined how much of the current view was occupied by the current clicked
// on zoom-to object. If mostly in view, assume we want to "de-focus" (zoom
// back out to the map from our "virtual focal" zoomed-to node), but if mostly
// not in view, re-center on this object. When last tested, this was smart
// enough to allow you to simply cycle through a stack of slide-icons with
// double clicking on the exposed edge of the nearby slide icons (of course,
// this code was on LWSlide back then...)
if (DEBUG.Enabled) {
outf(" overlapWidth %4.1f%%", overlapWidth * 100);
outf("overlapHeight %4.1f%%", overlapHeight * 100);
outf("clipped=" + clipped);
}
if (clipped) {
focusNode = true;
} else if (overlapWidth > 0.8 || overlapHeight > 0.8) {
focusNode = false;
} else
focusNode = true;
if (focusNode) {
viewer.clearRollover();
if (SwapFocalOnSlideZoom) {
// loadfocal animate only currently works when popping (to a parent focal)
//viewer.loadFocal(this, true, AnimateOnZoom);
ZoomTool.setZoomFitRegion(viewer,
mapBounds,
0,
AnimateOnZoom);
viewer.loadFocal(this);
} else {
ZoomTool.setZoomFitRegion(viewer,
mapBounds,
-LWPathway.PathBorderStrokeWidth / 2,
AnimateOnZoom);
}
} else {
// just re-fit to the map
viewer.fitToFocal(AnimateOnZoom);
}
return true;
}
/** any XML tag found in a save file that does not match a mapping in from the current mapping file shows
* up here -- they appear to always be instances of org.exolab.castor.types.AnyNode */
public void addObject(final Object o) {
final String s = o.toString();
final String name;
if (o instanceof String)
name = "String[";
else
name = Util.tag(o);
if (s.length() > 1024)
Log.info(this + " ignoring XML: " + name + "[" + s.substring(0,1024) + "...x" + s.length());
else
Log.info(this + " ignoring XML: " + name + s + "]");
}
/** interface {@link XMLUnmarshalListener} -- does nothing here */
public void XML_initialized(Object context) {
mXMLRestoreUnderway = true;
}
public void XML_fieldAdded(Object context, String name, Object child) {
if (DEBUG.XML && DEBUG.META) out("XML_fieldAdded <" + name + "> = " + child);
}
/** interface {@link XMLUnmarshalListener} */
public void XML_addNotify(Object context, String name, Object parent) {
if (DEBUG.XML && DEBUG.META) tufts.Util.printClassTrace("tufts.vue", "XML_addNotify; name=" + name
+ "\n\tparent: " + parent
+ "\n\t child: " + this
+ "\n");
// TODO: moving this layout from old position at end of LWMap.completeXMLRestore
// to here may have unpredictable results... watch of bad states after restores.
// The advantage of doing it here is that virtual children are handled,
// and "off map" children, such as slide children are properly handled.
//layout("XML_addNotify");
}
/** interface {@link XMLUnmarshalListener} -- call's layout */
public void XML_completed(Object context) {
// 2007-06-12 SMF -- do NOT turn this off yet -- let the LWMap
// turn it off when EVERYONE is done.
//mXMLRestoreUnderway = false;
/*
// TODO: TEMPORARY DEBUG: never restore slides as format changes at moment
//mSlides.clear();
for (LWSlide slide : mSlides.values()) {
// slides are virtual children of the node: we're their
// parent, tho they're not formal children of ours.
slide.setParent((LWContainer)this);
// TODO: currently, this means non-container objects, such as LWImages,
// can't have slides -- prob good to remove that restriction.
// What would break if the parent ref were just a LWComponent?
}
*/
if (DEBUG.XML) System.out.println("XML_completed " + this);
//layout(); need to wait till scale values are all set: so the LWMap needs to trigger this
}
/** clear the restore underway bit */
public void markAsRestored() {
mXMLRestoreUnderway = false;
}
protected static final double OPAQUE = 1.0;
/**
* @param alpha -- an alpha value for the whole image
* @param maxSize -- if non-null, the max width/height of the produced image (may be smaller)
* @param zoom -- a zoom for the map size in producing the image (currently ignored if maxSize is provided)
*/
protected BufferedImage getAsImage(double alpha, Dimension maxSize, double zoom) {
return createImage(alpha, maxSize, (Color) null, zoom);
}
public BufferedImage getAsImage(double alpha, Dimension maxSize) {
return getAsImage(alpha, maxSize, 1.0);
}
public BufferedImage getAsImage(double zoom) {
return getAsImage(OPAQUE, null, zoom);
}
public BufferedImage getAsImage() {
return getAsImage(OPAQUE, null, 1.0);
}
public BufferedImage createImage(double alpha, Dimension maxSize) {
return createImage(alpha, maxSize, null, 1.0);
}
/** @return the map bounds to use for rendering when generating an image of this LWComponent */
protected Rectangle2D.Float getImageBounds() {
final Rectangle2D.Float bounds = (Rectangle2D.Float) getPaintBounds().clone();
int growth = 1; // just in case / rounding errors
if (this instanceof LWMap)
growth += 15;
if (growth > 0)
grow(bounds, growth);
return bounds;
}
private static double computeZoomAndSize(Rectangle2D.Float bounds, Dimension maxSize, double zoomRequest, Size sizeResult)
{
double fitZoom = 1.0;
if (maxSize != null) {
if (bounds.width > maxSize.width || bounds.height > maxSize.height) {
fitZoom = ZoomTool.computeZoomFit(maxSize, 0, bounds, null);
sizeResult.width = (float) Math.ceil(bounds.width * fitZoom);
sizeResult.height = (float) Math.ceil(bounds.height * fitZoom);
}
} else if (zoomRequest != 1.0) {
sizeResult.width *= zoomRequest;
sizeResult.height *= zoomRequest;
fitZoom = zoomRequest;
}
return fitZoom;
}
/**
* Create a new buffered image, of max dimension maxSize, and render the LWComponent
* (and all it's children), to it using the given alpha.
* @param alpha 0.0 (invisible) to 1.0 (no alpha)
* @param maxSize max dimensions for image. May be null. Image may be smaller than maxSize.
* @param fillColor -- if non-null, will be rendered as background for image. If null, presume alpha 0 fill.
* @param zoomRequest -- desired zoom; ignored if maxSize is non-null
* also set, background fill will have transparency of alpha^3 to enhance contrast.
*/
// Note: as of Mac OS X 10.4.10 (Intel), when a java drag source declares it can
// generate an image (as we do when we Apple-drag something), if you drop it on the
// desktop, it will create a special mac "picture clipping", which is some kind of
// raw format, probabaly TIFF, tho you CANNOT open these in Preview. Apparently
// there's some kind of bug in the special .pictClipping, where sometimes when
// opening it up it shows entirely as a blank space (I think if the image starts to
// get "very large"), tho the data is actually there -- if you drag the picture
// clipping into an Apple Mail message, it shows up again (and if you dragged from
// VUE to Apple Mail in the first place, it also works fine). Note that AFTER
// dragging into Apple Mail, you can THEN double-click the attachment, and it will
// open it up in Preview as a .tiff file (Apple Mail appears to be converting the
// .pictClipping to tiff). Note that uncompressed TIFF isn't exactly a friendly
// mail attachment format as it's huge. But once you open the image in Preview, you
// have the option of saving it / exporting it as a jpeg, and you can even adjust
// the quality to your liking.
public BufferedImage createImage(double alpha, Dimension maxSize, Color fillColor, double zoomRequest)
{
final Rectangle2D.Float bounds = getImageBounds();
if (DEBUG.IMAGE) {
System.out.println();
System.out.println(TERM_CYAN +
"createImage: " + this
+ "\n\t zoomRequst: " + zoomRequest
+ "\n\t maxSize: " + maxSize
+ "\n\t mapBounds: " + fmt(bounds)
+ "\n\t fillColor: " + fillColor
+ "\n\t alpha: " + alpha
+ TERM_CLEAR
);
}
final Size imageSize = new Size(bounds);
final double usedZoom = computeZoomAndSize(bounds, maxSize, zoomRequest, imageSize);
// Image type ARGB is needed if at any point in the generated image, there is a
// not 100% opaque pixel all the way through the background. So TYPE_INT_RGB
// will handle transparency with a map fine -- but we need TYPE_INT_ARGB if,
// say, we're generating drag image that we want to be a borderless node (fully
// transparent image border), or if the whole drag image itself is
// semi-transparent.
final int imageType;
final int transparency;
if (fillColor == null || alpha != OPAQUE || fillColor.getAlpha() != 255) {
imageType = BufferedImage.TYPE_INT_ARGB;
transparency = Transparency.TRANSLUCENT;
} else {
imageType = BufferedImage.TYPE_INT_RGB;
transparency = Transparency.OPAQUE;
}
// final boolean fillHasAlpha = (fillColor != null && fillColor.getAlpha() != 255);
// //if (alpha == OPAQUE && fillColor != null && fillColor.getAlpha() == 255) {
// if (alpha == OPAQUE && (fillColor == null || fillColor.getAlpha() == 255)) {
// imageType = BufferedImage.TYPE_INT_RGB;
// transparency = Transparency.OPAQUE;
// } else {
// imageType = BufferedImage.TYPE_INT_ARGB;
// transparency = Transparency.TRANSLUCENT;
// }
final int width = imageSize.pixelWidth();
final int height = imageSize.pixelHeight();
if (width >= 512 || height >= 512)
Log.info("creating large image: " + imageSize + " = approx " + Util.abbrevBytes(width * height * 4));
try {
Log.info(this + "; createImage:"
+ "\n\t requestSize: " + imageSize
+ "\n\trequestAlpha: " + alpha
+ "\n\t requestFill: " + fillColor
+ "\n\t pixelSize: " + width + "x" + height
+ "\n\t renderScale: " + usedZoom
+ "\n\t type: " + (imageType == BufferedImage.TYPE_INT_RGB ? "RGB (opaque)" : "ARGB (translucent)")
);
} catch (Throwable t) {
Log.error("logging", t);
}
// if (DEBUG.IMAGE) out(TERM_CYAN
// + "createImage:"
// //+ "\n\tfinal size: " + width + "x" + height
// + "\n\t neededSize: " + imageSize
// + "\n\t usedZoom: " + usedZoom
// + "\n\t type: " + (imageType == BufferedImage.TYPE_INT_RGB ? "OPAQUE" : "TRANSPARENT")
// + TERM_CLEAR);
if (mImageBuffer != null &&
mImageBuffer.getWidth() == width &&
mImageBuffer.getHeight() == height &&
mImageBuffer.getType() == imageType)
{
// todo: could also re-use if cached image is > our needed size as long it's
// an ARGB and we fill it with full alpha first, tho we really shouldn't
// have each component caching it's own image: some kind of small
// recently used image buffers cache would make more sense.
if (DEBUG.DND || DEBUG.IMAGE) out(TERM_CYAN + "\ngot cached image: " + mImageBuffer + TERM_CLEAR);
} else {
try {
// TODO: manage this in a separate cache -- not one per node
mImageBuffer = tufts.vue.gui.GUI.getDeviceConfigForWindow(null)
.createCompatibleImage(width, height, transparency);
} catch (Throwable t) {
Log.error("creating image", t);
Log.error("creating image: failing node: " + Util.tags(this));
return null;
}
if (DEBUG.DND || DEBUG.IMAGE)
out(TERM_RED + "created image: " + mImageBuffer + TERM_CLEAR);
else
Log.info("created image " + mImageBuffer);
}
drawImage((Graphics2D) mImageBuffer.getGraphics(),
alpha,
maxSize,
fillColor,
zoomRequest
);
return mImageBuffer;
}
/**
* Useful for drawing drag images into an existing graphics buffer, or drawing exportable images.
*
* @param alpha 0.0 (invisible) to 1.0 (no alpha -- completely opaque)
* @param maxSize max dimensions for image. May be null. Image may be smaller than maxSize.
* @param fillColor -- if non-null, will be rendered as background for image.
* @param zoomRequest -- desired zoom; ignored if maxSize is non-null
* also set, background fill will have transparency of alpha^3 to enhance contrast.
*/
public void drawImage(Graphics2D g, double alpha, Dimension maxSize, Color fillColor, double zoomRequest)
{
//if (DEBUG.IMAGE) out("drawImage; size " + maxSize);
final boolean drawBorder = false;// this instanceof LWMap; // hack for dragged images of LWMaps
final Rectangle2D.Float bounds = getImageBounds();
final Rectangle clip = g.getClipBounds();
final Size fillSize = new Size(bounds);
final double zoom = computeZoomAndSize(bounds, maxSize, zoomRequest, fillSize);
if (DEBUG.IMAGE) out(TERM_GREEN
+ "drawImage:"
+ "\n\t mapBounds: " + fmt(bounds)
+ "\n\t fill: " + fillColor
+ "\n\t maxSize: " + maxSize
+ "\n\t zoomRequest: " + zoomRequest
+ "\n\t fitZoom: " + zoom
+ "\n\t fillSize: " + fillSize
+ "\n\t gc: " + g
+ "\n\t clip: " + fmt(clip)
+ "\n\t alpha: " + alpha
+ TERM_CLEAR
);
final int width = fillSize.pixelWidth();
final int height = fillSize.pixelHeight();
final DrawContext dc = new DrawContext(g, this);
dc.setInteractive(false);
if (alpha == OPAQUE) {
dc.setPrintQuality();
} else {
// if alpha, assume drag image (todo: better specified as an argument)
dc.setDraftQuality();
}
dc.setBackgroundFill(getRenderFillColor(null)); // sure we want null here?
dc.setClipOptimized(false); // always draw all children -- don't bother to check bounds
if (DEBUG.IMAGE) out(TERM_GREEN + "drawImage: " + dc + TERM_CLEAR);
if (fillColor != null) {
// if (false && alpha != OPAQUE) {
// Color c = fillColor;
// // if we have an alpha and a fill, amplify the alpha on the background fill
// // by changing the fill to one that has alpha*alpha, for a total of
// // alpha*alpha*alpha given our GC already has an alpha set.
// fillColor = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (alpha*alpha*255+0.5));
// }
if (alpha != OPAQUE)
dc.setAlpha(alpha, AlphaComposite.SRC); // erase any underlying in cache
if (DEBUG.IMAGE) out("drawImage: fill=" + fillColor);
g.setColor(fillColor);
g.fillRect(0, 0, width, height);
} else { //if (alpha != OPAQUE) {
// we didn't have a fill, but we have an alpha: make sure any cached data is cleared
// todo?: if fill is null, we need to clear as well -- it means we have implied alpha on any non-drawn bits
// TODO: if this is a selection drag, we usually want to fill with the map color (or ideally, the color
// of the common parent, e.g., a slide, if there's one common parent)
dc.g.setComposite(AlphaComposite.Clear);
g.fillRect(0, 0, width, height);
}
//if (alpha != OPAQUE)
dc.setAlpha(alpha, AlphaComposite.SRC);
if (DEBUG.IMAGE && DEBUG.META) {
// Fill the entire imageable area
g.setColor(Color.green);
g.fillRect(0,0, Short.MAX_VALUE, Short.MAX_VALUE);
}
final AffineTransform rawTransform = g.getTransform();
if (zoom != 1.0)
dc.g.scale(zoom, zoom);
// translate so that the upper left corner of the map region
// we're drawing is at 0,0 on the underlying image
g.translate(-bounds.getX(),
-bounds.getY());
// GC *must* have a bounds set or we get NPE's in JComponent (textBox) rendering
dc.setMasterClip(bounds);
if (DEBUG.IMAGE && DEBUG.META) {
// fill the clipped area so we can check our clip bounds
dc.g.setColor(Color.red);
dc.g.fillRect(-Short.MAX_VALUE/2,-Short.MAX_VALUE/2, // larger values than this can blow out internal GC code and we get nothing
Short.MAX_VALUE, Short.MAX_VALUE);
}
if (this instanceof LWImage) {
// for some reason, raw images don't seem to want to draw unless we fill first
dc.g.setColor(Color.white);
dc.g.fill(bounds);
}
// render to the image through the DrawContext/GC pointing to it
draw(dc);
if (drawBorder) {
g.setTransform(rawTransform);
//g.setColor(Color.red);
//g.fillRect(0,0, Short.MAX_VALUE, Short.MAX_VALUE);
if (DEBUG.IMAGE) {
g.setColor(Color.black);
dc.setAntiAlias(false);
} else
g.setColor(Color.darkGray);
g.drawRect(0, 0, width-1, height-1);
}
if (DEBUG.IMAGE) out(TERM_GREEN + "drawImage: completed\n" + TERM_CLEAR);
}
private String cleanControlChars(String s) {
if (s == null)
return null;
String patternString = "";
for(int i =0;i<9;i++) {
patternString += "(\\u000"+i+")|";
}
//u000D = carriage return
//u000C = form feed
//u000A = line feed
/// = tab
//(\\u000F)| = tab
// = tab
patternString += "(\\u000B)|(\\u000F)|(\\u0010)|(\\u0011)|(\\u0012)|(\\u0013)|(\\u0014)";
patternString += "(\\u0015)|(\\u0016)|(\\u0017)";
Pattern control = Pattern.compile(patternString); // need to make this better
Matcher m = control.matcher(s);
s= m.replaceAll("");
// todo performance: if no modifications are made, pass
// back the same passed in object
return s;
}
/** subclasses override this to add info to toString()
(return super.paramString() + new info) */
public String paramString()
{
if (hasFlag(Flag.STYLE) || hasFlag(Flag.DATA_STYLE)) {
return ColorToDebugString(getFillColor());
} else {
return String.format(" %+4.0f,%+4.0f %3.0fx%-3.0f", getX(), getY(), width, height);
}
}
protected void out(String s) {
//if (DEBUG.Enabled) Log.debug(s + "; " + this);
// String typeName = getClass().getSimpleName();
// if (typeName.startsWith("LW"))
// typeName = typeName.substring(2);
//if (DEBUG.Enabled) LWLog.debug(String.format("%6s[%-12.12s] %s", typeName, getDisplayLabel(), s));
LWLog.debug(String.format("%s %s", this, s));
}
protected void outf(String format, Object ... args) {
Util.outf(Log, format, args);
}
public String toString()
{
String typeName = getClass().getSimpleName();
if (typeName.startsWith("LW"))
typeName = typeName.substring(2);
String label = "";
String s;
if (getLabel() != null) {
label = " " + Util.tags(getDisplayLabel());
// if (true||isAutoSized())
// label = " \"" + getDisplayLabel() + "\"";
// else
// label = " (" + getDisplayLabel() + ")";
}
if (getID() == null) {
s = String.format("%-15s[",
String.format("%s.%08x", typeName, System.identityHashCode(this))
//typeName + "." + Integer.toHexString(System.identityHashCode(this))
);
//s += tufts.Util.pad(9, Integer.toHexString(hashCode()));
} else {
s = String.format("%6s[%-8s", typeName, getID());
//s = String.format("%-17s", typeName + "[" + getID());
//s += tufts.Util.pad(4, getID());
}
//if (this.scale != 1f) s += "z" + this.scale + " ";
s += describeBits();
s += " " + paramString();
if (getScale() != 1f) s += String.format(" z%.2f", getScale());
// if (mHideBits != 0) s += " " + getDescriptionOfSetBits(HideCause.class, mHideBits);
// if (mFlags != 0) s += " " + getDescriptionOfSetBits(Flag.class, mFlags);
s += label;
if (getResource() != null)
s += " " + getResource().getSpec();
//s += " <" + getResource() + ">";
s += "]";
return s;
}
protected String describeBits() {
String s = "";
if (mHideBits != 0) s += " " + getDescriptionOfSetBits(HideCause.class, mHideBits);
if (mFlags != 0) {
if (mHideBits != 0)
s += " ";
s += getDescriptionOfSetBits(Flag.class, mFlags);
}
return s;
}
public static void main(String args[]) throws Exception
{
VUE.init(args);
/*
for (java.lang.reflect.Field f : LWComponent.class.getDeclaredFields()) {
Class type = f.getType();
if (type == Key.class)
System.out.println("KEY: " + f);
else
System.out.println("Field: " + f + " (" + type + ")");
}
*/
// for debug: ensure basic LW types created first
new LWNode();
new LWLink();
new LWImage();
//NodeTool.getTool();
VueToolbarController.getController(); // make sure the tools are initialized
edu.tufts.vue.style.StyleReader.readStyles("compare.weight.css");
java.util.Set<String> sortedKeys = new java.util.TreeSet<String>(edu.tufts.vue.style.StyleMap.keySet());
for (String key : sortedKeys) {
final Object style = edu.tufts.vue.style.StyleMap.getStyle(key);
System.out.println("Found CSS style key; " + key + ": " + style);
//System.out.println("Style key: " + se.getKey() + ": " + se.getValue());
}
new LWNode().applyCSS(edu.tufts.vue.style.StyleMap.getStyle("node.w1"));
new LWLink().applyCSS(edu.tufts.vue.style.StyleMap.getStyle("link.w1"));
}
protected static final org.apache.log4j.Logger LWLog = org.apache.log4j.Logger.getLogger(LW.class);
}
/** for debug */
final class LW {}
/*
private final java.lang.reflect.Field field;
public Key(String name, String fieldName) {
this(name);
// this successfully auto-generates the slot reference, tho not really worth
// it, as requiring the extra code snippet for grabbing the slot (Property)
// object at least eliminates any typo's. If we were to bother with this,
// we'd want to generate a Field ref to an actual member field that had the
// real value, and wasn't a slot. Then the renderers, etc, could get
// directly at the real value without using the slot -- a tad faster. Then
// stuff like the auto-notify code would all need to happen in the key, tho
// then all our "traditional" setters (for hand-coding convenience, and at
// least for save file backward compat) would need to use the Key to do the
// setting for the appropriate triggers (except for "take" usage)
java.lang.reflect.Field f = null;
if (fieldName != null) {
try {
f = LWComponent.class.getField(fieldName);
System.out.println("Found field: " + f);
} catch (Throwable t) {
tufts.Util.printStackTrace(t);
}
}
field = f;
}
Property getSlot(LWComponent c) {
try {
return (Property) field.get(c);
} catch (Throwable t) {
tufts.Util.printStackTrace(t);
}
return null;
}
*/