/*
* 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 tufts.vue.DEBUG;
import tufts.vue.NodeTool.NodeModeTool;
import edu.tufts.vue.preferences.implementations.BooleanPreference;
import java.io.IOException;
import java.util.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.geom.*;
import javax.swing.Icon;
/**
* Provides for the managing of a list of LWComponents as elements in a "path" through
* the map as well as ability to render that path on the map. Includes a current
* "index", which isn't just the current component, because components can appear
* in the path at multiple locations (no restrictions, tho we should probably
* restrict components appearing right next to each other in the path as I don't
* see how that would be useful). Also provides for associating path-specific notes
* for each component in that path (notes are NOT currently index specific, only
* component specific per path). --SF
*
* @author Scott Fraize
* @version $Revision: 1.234 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $
*/
public class LWPathway extends LWContainer
implements LWComponent.Listener
{
private static final int NO_INDEX_CHANGE = Short.MIN_VALUE;
private static boolean ShowSlides = true;
private int mCurrentIndex = -1;
private MasterSlide mMasterSlide;
private boolean mRevealer = false;
/** For PathwayTable: does this pathway currently show it's entries in the list? */
private transient boolean mOpen = true;
/** The pathway Entry's */
private java.util.List<Entry> mEntries = new java.util.ArrayList(); // could re-use from LWComponent, but may be dangerous
/** Read-only version of the entry list */
private java.util.List<Entry> mSecureEntries = java.util.Collections.unmodifiableList(mEntries);
/** for backward compat with old style saved pathways -- the ordered list of member node ID's */
private transient java.util.List<String> mOldStyleMemberIDList = new java.util.ArrayList();
/** for backward compat with old style saved pathways (pre Feb/March 2006) */
private ArrayList<LWPathwayElementProperty> mOldStyleProperties = new ArrayList();
// todo: encapsulating the preference with the code that needs it is nice, but it's
// actually a very bad idea, as this means the order of the preferences in the
// preference pane will be determined by how we load classes, which could even
// theoretically change from one startup to the next. Not to mention, if we declare
// one in a class that isn't loaded at all during startup, the preference will be
// missing until it's loaded. Create a tufts.vue.Prefs.java to put these in.
private static final BooleanPreference AutoNodeToSlideNotesPref = BooleanPreference.create
(edu.tufts.vue.preferences.PreferenceConstants.PRESENTATION_CATEGORY,
"autoCopyNotes",
VueResources.getString("preferences.notes.title"),
VueResources.getString("preferences.notes.descriptionone"),
VueResources.getString("preferences.notes.descriptiontwo"),
Boolean.TRUE,
true);
/**
* This encapsulates the data for each pathway entry. A node can be repeated in a
* pathway as many times as the user likes, each with it's own notes, slide,
* etc. Also, an entry in a pathway can just be a slide, and not be associated with
* a particular node. A special version of this class is also used as a convenience
* for the PathwayTableModel and VUE.ActivePathwayEntryListener, so that the rows in
* PathwayTable can all be instances of this class (e.g., both node entries and the
* pathways themselves). This special version of the entry represents the pathway
* itself.
*/
// todo: better if this class was in it's own file, including all the auto-slide creation & synchronization code
public static class Entry implements Transferable {
public static final String MAP_VIEW_CHANGED = "pathway.entry.mapView";
/** the pathway this entry is in */
public final LWPathway pathway;
/** can be null if slide is "pathway only" combination of other nodes
* Will be the same as pathway if this is a special entry for the pathway itself (for PathwayTableModel)
* -- should be final, but castor doesn't support that */
public /*final*/ LWComponent node;
/** the slide object -- may be null if isMapSlide is true, and we've never created a slide here */
LWSlide slide;
/** if true, we don't want to use a slide -- just display the node on the map */
boolean isMapView = true; // default true so old pre-presentation save files don't immediately create a huge batch of slides
/** notes for this pathway entry */
String notes = "";
/** runtime-only cached MapSlide */
transient LWSlide mapSlide;
private Entry(LWPathway pathway, LWComponent node) {
this.pathway = pathway;
this.node = node;
syncNodeEntryRef();
}
// /** create a merge of multiple nodes */
// private Entry(LWPathway pathway, Iterable<LWComponent> contents) {
// this.pathway = pathway;
// this.node = null;
// String titleText = "Untitled Slide";
// this.slide = LWSlide.CreateForPathway(pathway, titleText, null, contents, true);
// this.slide.enableProperty(LWKey.Label);
// this.slide.setPathwayEntry(this);
// this.setLabel(titleText);
// syncNodeEntryRef();
// }
/** for our use during castor restores */
private Entry(LWPathway pathway, Entry partial) {
this.pathway = pathway;
this.node = partial.node;
this.slide = partial.slide;
if (slide != null)
slide.setPathwayEntry(this);
this.isMapView = partial.isMapView;
this.notes = partial.notes;
if (isOffMapSlide() && slide != null)
slide.enableProperty(LWKey.Label);
syncNodeEntryRef();
}
/** for castor's use during restores */
public Entry() {
pathway = null;
}
/** @return this index in the pathway, starting at 0 */
public int index() {
return pathway.getEntryIndex(this);
}
/** @return next entry on this pathway, or null if at end */
public Entry next() {
return pathway.getEntry(index() + 1);
}
/** @return prev entry on this pathway, or null if at beginning */
public Entry prev() {
return pathway.getEntry(index() - 1);
}
/** @return true if this is the last entry on the pathway */
public boolean isLast() {
return pathway.getLast() == this;
}
/** @return true if this is the first entry on the pathway */
public boolean isFirst() {
return pathway.getFirst() == this;
}
private final boolean restoreUnderway() {
return pathway == null;
}
private void syncNodeEntryRef() {
if (node != null && node instanceof LWPathway == false)
node.addEntryRef(this);
}
/** Make sure the pathway is listening to the given LWComponent, and that our node knows it knows it's in this pathway / has this entry */
private void ensureModel() {
if (node == null)
return;
if (DEBUG.UNDO && DEBUG.META) pathway.out("ensureModel " + this);
node.addEntryRef(this);
if (slide != null)
slide.setPathwayEntry(this);
node.addLWCListener(pathway, LWKey.Deleting, LWKey.Label, LWKey.Hidden);
}
/** Stop the pathway from listening to the given LWComponent, and tell it it's no longer in this pathway / has this entry */
private void removeFromModel() {
if (node == null)
return;
if (DEBUG.UNDO && DEBUG.META) pathway.out("removeFromModel " + this);
node.removeEntryRef(this);
node.removeLWCListener(pathway);
if (node instanceof LWPortal && node.getEntries().size() == 0) {
// if this portal has no other entries, remove it
node.getParent().deleteChildPermanently(node);
}
// TODO: do we still need to listen to each of our members?
// if slides stay as children of LWPathway, could handle
// via broadcastChildEvent
}
public String getLabel() {
if (node != null)
return node.getDisplayLabel();
else
return getSlide().getLabel();
}
public void setLabel(String s) {
if (node != null)
node.setLabel(s);
else
getSlide().setLabel(s);
}
public LWComponent getFocal() {
return isMapView() ? node : (canProvideSlide() ? getSlide() : node); // For VUE-967
//return canProvideSlide() ? getSlide() : node;
}
public Color getFullScreenFillColor(DrawContext dc) {
if (isMapView())
return pathway.getMasterSlide().getFillColor();
else
return getSlide().getRenderFillColor(dc);
}
public boolean hasSlide() {
return canProvideSlide();
//return !isMapView();
}
public LWSlide getSlide() {
return getSlide(false);
}
/** force a slide no matter what: e.g., for printing */
public LWSlide produceSlide() {
return getSlide(true);
}
private LWSlide getSlide(boolean force) {
if (isMapView()) {
if (force || node.supportsSlide()) {
//if (slide == null || slide instanceof MapSlide == false)
if (mapSlide == null)
mapSlide = new MapSlide(this);
return mapSlide;
} else
return null;
}
if (slide == null || slide instanceof MapSlide)
buildSlide();
if (node != null && slide.parent != node) {
if (node instanceof LWContainer)
slide.setParent((LWContainer)node);
else
Util.printStackTrace("Non-containers can't have slides: " + node + " can't own " + slide);
}
return slide;
}
private void buildSlide() {
// TODO: check/test undo -- is it working / the mark happening at the right time?
final LWSlide oldSlide = slide;
slide = Slides.CreateForPathwayEntry(this);
pathway.notify("slide.rebuild", new Undoable() { void undo() {
slide = oldSlide;
}});
}
public void rebuildSlide() {
buildSlide();
}
/** @return what should be selected for this entry: will be node for map-view, slide otherwise, which may
* currently be null if hasn't been created yet */
// This could return the node instead of the slide if the slide-icon's aren't
// currently visible, tho it would be better to force showing the hidden
// slide icon even if there off...
public LWComponent getSelectable() {
return (canProvideSlide() && getSlide().isVisible()) ? getSlide() : node;
//return canProvideSlide() ? getSlide() : node;
//return isMapView() ? node : slide;
}
public void revertSlideToMasterStyle() {
if (slide != null)
slide.revertToMasterStyle();
}
/** for castor: don't build a slide if we haven't got one */
public LWSlide getPersistSlide() {
return isMapView() ? null : slide;
}
/** for castor: don't build a slide if we haven't got one */
public void setPersistSlide(LWSlide s) {
if (isMapView) {
if (DEBUG.Enabled)
Log.info("skipping restoring slide for an entry marked as map-view: " + this);
//Util.printStackTrace("skipping restoring slide for an entry marked as map-view: " + this);
slide = null;
} else
slide = s;
}
public void setMapView(boolean asMapView) {
if (isMapView == asMapView)
return;
final boolean wasMapView = isMapView;
isMapView = asMapView;
if (restoreUnderway())
return;
// So the PathwayTableModel knows to update / PathwayTable knows to redraw
pathway.notify(this, MAP_VIEW_CHANGED);
// Anyone listening to the old focal (e.g., a MapViewer), can watch for the
// MAP_VIEW_CHANGED event on that old focal (e.g., the user slide, virtual
// slide, or node itself), to know it's entry type has changed, so they can
// change views if they want to change when the entry changes.
if (wasMapView) {
if (mapSlide != null) {
mapSlide.notify(this, MAP_VIEW_CHANGED);
if (VUE.getActiveComponent() == mapSlide) {
// the MapView slide is no longer the slide for this
// entry: select the new slide
VUE.setActive(LWComponent.class, this, getSlide());
}
}
else if (node != null)
node.notify(this, MAP_VIEW_CHANGED);
} else {
// was regular slide view:
if (slide != null) slide.notify(this, MAP_VIEW_CHANGED);
if (VUE.getActiveComponent() == slide) {
// the regular slide is no longer the slide for this
// entry: select the MapView slide
VUE.setActive(LWComponent.class, this, getSlide());
}
}
// During restores, until node is set, we always think we're a merged slide, and isMapView never gets restored!
// This is just a redundancy check anyway for runtime testing.
// if (asMapView && isOffMapSlide()) {
// tufts.Util.printStackTrace("merged slide can't have map view");
// } else {
// isMapView = asMapView;
// }
}
public final boolean isMapView() {
// todo: node.isMapViewOnly or node.supportsSlide
//if (node instanceof LWPortal || node instanceof LWSlide)
if (node.supportsSlide())
return isMapView;
else
return true;
}
public boolean canProvideSlide() {
if (isMapView())
return node.supportsSlide();
else
return true;
}
public final boolean hasVisibleSlide() {
return pathway.isShowingSlides() && canProvideSlide();
// if (!pathway.isShowingSlides())
// return false;
// if (isMapView())
// return node.supportsSlide();
// else
// return true;
//return !isMapView() && pathway.isShowingSlides();
//return !isMapView && pathway.isShowingSlides();
}
public final boolean isPortal() {
return (node instanceof LWPortal)
|| (node != null && node.isTranslucent());
}
/** @return false for now: merged slides are not super-special at the moment -- they always have a node behind on the map */
public final boolean isOffMapSlide() {
return false;
//return node == null;
}
/** @return true if this entry can support more than one presentation display mode
* (e.g., a map/"raw node" view and a slide view)
*/
public boolean allowsMultipleDisplayModes() {
return node.supportsSlide() && !isOffMapSlide();
//return !isOffMapSlide() && !(node instanceof LWPortal);
}
@Deprecated
public final boolean hasVariableDisplayMode() {
return allowsMultipleDisplayModes();
}
/** @return true if there is a map node associated with this entry, and it should only
* be visible when the pathway is visible.
*/
public final boolean hidesWithPathway() {
return node instanceof LWPortal;
}
public boolean hasNotes() {
return notes != null && notes.length() > 0;
}
public String getNotes() {
return notes;
}
public String XMLgetNotes() {
return LWComponent.escapeWhitespace(this.notes);
}
public void XMLsetNotes(String notes) {
setNotes(LWComponent.decodeCastorMultiLineText(notes));
}
public void setNotes(String s) {
if (notes == s || (notes != null && notes.equals(s)))
return;
final String oldNotes = notes;
notes = s;
if (pathway != null) // will be null during restore
pathway.notify("pathway.entry.notes", new Undoable() { void undo() { setNotes(oldNotes); }});
// TODO: this isn't updating on undo / remote call
}
public boolean isPathway() { return false; }
public boolean isVisibleOnMap() {
return pathway.isVisible() && node.isDrawn();
}
public String toString() {
String s = "Entry["
+ (pathway == null ? "<null pathway>" : pathway.getLabel())
+ "#" + (pathway == null ? -999 : index())
+ "; " + node + " ";
if (isMapView())
s += "MAP-VIEW";
else
s += (slide == null ? "<null-LWSlide>" : slide.getLabel());
return s + ']';
}
public Object getTransferData(java.awt.datatransfer.DataFlavor arg0) throws UnsupportedFlavorException, IOException {
// TODO Auto-generated method stub
return this;
}
public java.awt.datatransfer.DataFlavor[] getTransferDataFlavors() {
// TODO Auto-generated method stub
DataFlavor[] list = new DataFlavor[1];
try
{
list[0] = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType);
}
catch(ClassNotFoundException cnf)
{
cnf.printStackTrace();
}
return list;
}
public boolean isDataFlavorSupported(java.awt.datatransfer.DataFlavor arg0) {
if (arg0.isFlavorRemoteObjectType())
return true;
else
return false;
}
}
private static final Color AlphaWhite = new Color(255,255,255,128);
public final Icon mSlideIcon = new Icon() {
public void paintIcon(java.awt.Component c, java.awt.Graphics g, int x, int y) {
//g.setColor(getMasterSlide().getFillColor());
final Color color = Util.alphaMix(AlphaWhite, getMasterSlide().getFillColor());
g.setColor(color);
g.fillRect(x, y, getIconWidth(), getIconHeight());
//g.setColor(Color.gray);
g.setColor(color.darker());
g.drawRect(x, y, getIconWidth(), getIconHeight());
// g.translate(x + 2, y + 3);
// g.drawLine(0, 0, 8, 0);
// g.drawLine(0, 2, 10, 2);
// g.drawLine(0, 4, 6, 4);
}
public int getIconWidth() { return 14; }
public int getIconHeight() { return 10; }
};
/**
* This special pathway entry represents the pathway itself.
* This is a very handy hack that allows the PathwayTable / PathwayTableModel only deal with Entry objects.
*/
private final Entry mOurEntry =
new Entry(LWPathway.this, LWPathway.this) {
@Override
public boolean canProvideSlide() { return false; }
@Override
public LWComponent getFocal() { return getMasterSlide(); }
@Override
public LWComponent getSelectable() { return getMasterSlide(); }
@Override
public boolean isPathway() { return true; }
@Override
public String getNotes() { return pathway.getNotes(); }
@Override
public void setNotes(String s) { pathway.setNotes(s); }
@Override
public boolean hasNotes() { return pathway.hasNotes(); }
@Override
public LWSlide getSlide() { return null; }
@Override
public boolean allowsMultipleDisplayModes() { return false; }
@Override
public int index() { return -1; }
@Override
public boolean isVisibleOnMap() { return false; }
};
public Entry asEntry() {
return mOurEntry;
}
LWPathway(String label) {
this(null, label);
}
/** Creates a new instance of LWPathway with the specified label */
public LWPathway(LWMap map, String label) {
initPathway();
setMap(map);
setLabel(label);
setStrokeColor(getNextColor());
}
private static final int PathwayAlpha = (int) (255f * (VueResources.getInt("pathway.alpha.percent", 50) / 100f) + 0.5);
private void initPathway() {
disablePropertyTypes(KeyType.STYLE);
mStrokeColor.setFixedAlpha(PathwayAlpha);
}
/** @return null -- will prevent participating in auto-styling system */
@Override
public Object getTypeToken() {
return null;
}
/** @return false: pathways can't be selected with anything else */
public boolean supportsMultiSelection() {
return false;
}
/** set the global show-slides state */
public static void setShowSlides(boolean showSlides) {
LWPathway.ShowSlides = showSlides;
}
/** toggle the global show-slides state */
public static void toggleSlideIcons() {
setShowSlides(!LWPathway.ShowSlides);
}
/** @return true if the global state for showing slide icons is set */
public static boolean isShowingSlideIcons() {
return ShowSlides;
}
/** @return true if *this* pathway is currently showing slides (it's currently visible, and slide icons are turned on) */
public boolean isShowingSlides() {
return ShowSlides && isDrawn();
}
/** @return the first Entry that is for the given LWComponent */
public Entry getFirstEntry(LWComponent c) {
for (Entry e : mEntries)
if (e.node == c)
return e;
return null;
}
public Entry getFirst() {
return mEntries.size() > 0 ? mEntries.get(0) : null;
}
public Entry getLast() {
return mEntries.size() > 0 ? mEntries.get(mEntries.size()-1) : null;
}
/**
* Is this a "reveal"-way? Members start hidden and are made visible as you move
* through the pathway. This value managed by LWPathwayList, as only one Pathway
* per map is allowed to be an revealer at a time.
*
* @deprecated -- this functionality has been removed, at least for now...
*/
boolean isRevealer() {
return false;
//return mRevealer;
}
void setRevealer(boolean t) {
throw new UnsupportedOperationException("re-implement reveal functionality");
// mRevealer = t;
// updateMemberVisibility();
}
/** @return false -- cannot filter out entire pathways */
@Override public final boolean isFiltered() {
if (DEBUG.Enabled) Util.printStackTrace("isFiltered called " + this);
return false;
}
@Override public final void setFiltered(boolean t) {
if (DEBUG.Enabled) Util.printStackTrace("setFiltered " + t + "; " + this);
}
@Override public boolean hasDraws() {
return isVisible() && mEntries.size() > 0;
}
// @Override public boolean isDrawn() {
// return !isRevealer() // deprecated
// && (isVisible() && !isFiltered()) // old isDrawn
// && mEntries.size() > 0;
// //return !isRevealer() && super.isDrawn() && mEntries.size() > 0;
// }
@Override public void setVisible(boolean visible) {
if (DEBUG.PATHWAY) out("setVisible " + visible);
super.setVisible(visible);
updateMemberVisibility(visible);
/*
* Not currently using the reveal-way feature:
if (isRevealer()) {
if (visible) {
// if "showing" a reveal pathway, we actually hide all the
// elements after the current index
updateMemberVisibility();
} else {
if (DEBUG.PATHWAY) System.out.println(this + " setVisible: showing all items");
for (Entry e : mEntries) {
if (e.node != null) {
e.node.clearHidden(HideCause.PATH_UNREVEALED);
}
}
}
}
*/
}
/**
* Make sure any nodes that should hide/show with this pathway
* get a hidden bit set as needed.
*/
// was: for reveal-way's: show all members up to index, hide all post current index
private void updateMemberVisibility(boolean visible)
{
for (Entry e : mEntries) {
if (e.hidesWithPathway()) {
if (!visible) {
boolean hideNode = true;
// if this node is on any OTHER pathways, and they
// are visible, still keep the node visible...
for (LWPathway pathway : e.node.getPathways()) {
if (pathway.isVisible()) {
hideNode = false;
break;
}
}
e.node.setHidden(HideCause.HIDES_WITH_PATHWAY, hideNode);
} else {
e.node.clearHidden(HideCause.HIDES_WITH_PATHWAY);
}
}
}
/*
if (DEBUG.PATHWAY) System.out.println(this + " setVisible: hiding post-index items, showing all others");
int index = 0;
for (Entry e : mEntries) {
if (e.node == null)
continue;
if (isRevealer()) {
if (index > mCurrentIndex)
e.node.setHidden(HideCause.PATH_UNREVEALED);
else
e.node.clearHidden(HideCause.PATH_UNREVEALED);
index++;
} else {
e.node.clearHidden(HideCause.PATH_UNREVEALED);
}
}
*/
}
private static Color[] ColorTable = {
new Color(153, 51, 51),
new Color(204, 51, 204),
new Color(51, 204, 51),
new Color(51, 204, 204),
new Color(255, 102, 51),
new Color(51, 102, 204),
};
private static int sColorIndex = 0;
private static Color getNextColor()
{
if (sColorIndex >= ColorTable.length)
sColorIndex = 0;
return ColorTable[sColorIndex++];
}
public int getCurrentIndex() {
return mCurrentIndex;
}
public int firstIndexOf(LWComponent c) {
int index = 0;
for (Entry e : mEntries) {
if (e.node == c)
return index;
index++;
}
return -1;
}
public boolean contains(LWComponent c) {
return firstIndexOf(c) >= 0;
}
/** @return the number of times the given component appears in the pathway */
public int count(LWComponent c) {
int count = 0;
for (Entry e : mEntries)
if (e.node == c)
count++;
return count;
}
/** Set the current index to the first instance of LWComponent @param c in the pathway
*/
void setCurrentEntry(Entry e) {
setIndex(mEntries.indexOf(e));
}
public Entry getEntry(int index) {
if (index >= length() || index < 0)
return null;
else
return mEntries.get(index);
}
public int getEntryIndex(Entry e) {
if (e.pathway != this)
Log.warn("fetching entry index for non-member of ths pathway: " + e);
return mEntries.indexOf(e);
}
public Entry getCurrentEntry() {
return mCurrentIndex >= 0 ? getEntry(mCurrentIndex) : null;
}
public LWComponent getNodeEntry(int index) {
Entry e = getEntry(index);
return e == null ? null : e.node;
}
/** return the node at the current index -- may be null if current index is a pathway-only slide */
public LWComponent getCurrentNode() {
if (mCurrentIndex < 0)
return null;
return getNodeEntry(mCurrentIndex);
}
/*
public Entry getEntryWithSlide(LWSlide slide) {
for (Entry e : mEntries)
if (e.slide == slide)
return e;
return null;
}
*/
private List<Entry> cloneEntries() {
return (List<Entry>) ((ArrayList<Entry>)mEntries).clone();
}
public boolean moveEntry(int start, int end)
{
// if (mCurrentIndex <=0)
// return false;
final List<Entry> newEntries = cloneEntries();
Entry moveStart = getEntry(start);
newEntries.remove(moveStart);
newEntries.add(end,moveStart);
setEntries("pathway.reorder",newEntries,newEntries.size()-1);
return true;
}
public boolean moveCurrentUp()
{
if (mCurrentIndex <= 0)
return false;
final List<Entry> newEntries = cloneEntries();
Entry moveUp = getEntry(mCurrentIndex);
Entry moveDown = getEntry(mCurrentIndex - 1);
newEntries.set(mCurrentIndex, moveDown);
newEntries.set(mCurrentIndex - 1, moveUp);
setEntries("pathway.reorder", newEntries, mCurrentIndex-1);
return true;
}
public boolean moveCurrentDown()
{
if (mCurrentIndex == length()-1)
return false;
final List<Entry> newEntries = cloneEntries();
Entry moveDown = getEntry(mCurrentIndex);
Entry moveUp = getEntry(mCurrentIndex + 1);
newEntries.set(mCurrentIndex + 1, moveDown);
newEntries.set(mCurrentIndex, moveUp);
setEntries("pathway.reorder", newEntries, mCurrentIndex+1);
return true;
}
/**
* Set the current index to @param i, and also set the
* VUE selection to the component at that index.
* @return the index as a convenience
*/
public int setIndex(int i)
{
if (DEBUG.PATHWAY) out("setIndex " + i);
if (mCurrentIndex == i)
return i;
mCurrentIndex = i;
// if (isRevealer() && isVisible())
// updateMemberVisibility();
broadcastCurrentEntry();
// No longer need this as it's all handled via the setActive:
//notify("pathway.index"); // we need this so the PathwayTable is eventually told to redraw
// Although this property is actually saved, it doesn't seem worthy of having
// it be in the undo list -- it's more of a GUI config. (And FYI, I'm not sure if
// this property is being properly restored at the moment either).
//notify("pathway.index", new Undoable(old) { void undo(int i) { setIndex(i); }} );
return mCurrentIndex;
}
// /** make sure we're listening to the given LWComponent, and that it knows it's in this pathway */
// private void ensureMemberRefs(Entry e) {
// if (e.node == null)
// return;
// if (DEBUG.UNDO && DEBUG.META) out("ensureMemberRefs " + e);
// e.node.addPathwayRef(this);
// e.node.addEntryRef(e);
// e.node.addLWCListener(this, LWKey.Deleting, LWKey.Label, LWKey.Hidden);
// }
// /** Stop listening to the given LWComponent, and tell it it's no longer in this pathway */
// // TODO: merge with entry.removeModelRefs
// private void removeMemberRefs(Entry e) {
// if (e.node == null)
// return;
// if (DEBUG.UNDO && DEBUG.META) out("removeMemberRefs " + e);
// e.node.removePathwayRef(this);
// e.node.removeEntryRef(e);
// e.node.removeLWCListener(this);
// // TODO: do we still need to listen to each of our members?
// // if slides stay as children of LWPathway, could handle
// // via broadcastChildEvent
// }
/** and an entry for the given component at the end of the pathway */
public void add(LWComponent c) {
add(Util.iterable(c));
}
/** @return true if the given component can be added to a pathway */
public static boolean isPathwayAllowed(LWComponent c) {
if (c instanceof LWPathway ||
c instanceof LWMap || // just in case
c instanceof LWLink || // no longer allowed -- decided in staff 2007-08-29 -- SMF
//c instanceof LWText || // disallowed 2008-04-25 SMF: usually can't see them
c.isPathwayOwned() || // e.g.: slides appearing as slide icons
!c.isMoveable() || // just in case, don't allow any non-moveables
(c instanceof LWImage && ((LWImage)c).isNodeIcon()) ||
c.hasAncestorOfType(LWSlide.class))
return false;
else
return true;
}
/**
* Add new entries to the end of the pathway for all the components in the iterator.
*/
public void add(Iterator<LWComponent> i)
{
if (DEBUG.PATHWAY||DEBUG.PARENTING) out("add " + i);
List<Entry> newEntries = cloneEntries();
int addCount = 0;
while (i.hasNext()) {
final LWComponent c = i.next();
if (!isPathwayAllowed(c)) {
if (DEBUG.PATHWAY||DEBUG.PARENTING) out("DENIED ADDING " + c);
continue;
}
if (DEBUG.PATHWAY||DEBUG.PARENTING) out("adding " + c);
// if (c instanceof LWPathway) {
// if (c == this)
// Util.printStackTrace(this + ": Can't add a pathway to itself");
// else
// Util.printStackTrace(this + ": Can't add a pathway to another pathway: " + c);
// continue;
// }
final Entry e = new Entry(this, c);
if (AutoNodeToSlideNotesPref.isTrue())
e.setNotes(c.getNotes());
// these either require map view, or are likely to want to start that way
e.setMapView(c.isTranslucent() ||
c instanceof LWGroup ||
c instanceof LWPortal ||
c instanceof LWImage ||
c instanceof LWSlide ||
c instanceof LWLink
);
newEntries.add(e);
addCount++;
}
if (addCount > 0) {
int newIndex = NO_INDEX_CHANGE;
if (addCount == 1)
newIndex = newEntries.size() - 1;
setEntries("pathway.add", newEntries, newIndex);
}
}
/*
public void addMergedSlide(LWSelection selection)
{
final Collection<LWComponent> mergedContents = new LinkedHashSet(); // preserve's insertion order
for (LWComponent c : selection) {
mergedContents.add(c);
c.getAllDescendents(ChildKind.PROPER, mergedContents);
}
Entry e = new Entry(this, mergedContents);
List<Entry> newEntries = cloneEntries();
newEntries.add(e);
setEntries("pathway.add", newEntries, newEntries.size() - 1);
}
*/
public LWComponent createMergedNode(Collection<LWComponent> selection)
{
final Collection<LWComponent> mergedContents = new LinkedHashSet(); // preserve's insertion order
for (LWComponent c : selection) {
mergedContents.add(c);
c.getAllDescendents(ChildKind.PROPER, mergedContents, Order.TREE);
}
final LWNode node = NodeModeTool.createNewNode("Untitled"); // why can't we just use "NodeTool" here?
node.addChildren(Actions.duplicatePreservingLinks(mergedContents));
return node;
}
/** @param index is ignored if toRemove is non-null */
// TODO: isDeleting currently not used -- do we still need it?
private void removeEntries(final Iterator<LWComponent> toRemove, int index, boolean isDeleting)
{
if (DEBUG.PATHWAY||DEBUG.PARENTING) out("removeEntries " + toRemove + " index=" + index + " isDeleting="+isDeleting);
final List<Entry> newEntries = cloneEntries();
boolean didRemove = false;
if (toRemove == null) {
Entry e = newEntries.get(index);
newEntries.remove(index);
didRemove = true;
} else {
while (toRemove.hasNext()) {
LWComponent c = toRemove.next();
if (DEBUG.PATHWAY||DEBUG.PARENTING) out("removing " + c);
Iterator<Entry> i = newEntries.iterator();
while (i.hasNext()) {
if (i.next().node == c) {
i.remove();
didRemove = true;
}
}
}
}
if (didRemove)
setEntries("pathway.remove", newEntries, NO_INDEX_CHANGE);
}
private synchronized void removeIndex(int index, boolean isDeleting)
{
if (DEBUG.PATHWAY||DEBUG.PARENTING) out("removeIndex " + index + " isDeleting=" + isDeleting);
removeEntries(null, index, isDeleting);
}
/** remove only the FIRST reference to the given compoent in the pathway */
public void removeFirst(LWComponent c) {
removeIndex(firstIndexOf(c), false);
}
/**
* As a LWComponent may appear more than once in a pathway, we
* need to make sure we can remove pathway entries by index, and
* not just by content.
*/
public synchronized void remove(int index) {
removeIndex(index, false);
}
/**
* Pathways aren't true
* parents, so all we want to do is remove the reference to them
* and raise a change event. Removes all items in iterator
* COMPLETELY from the pathway -- all instances are removed.
* The iterator may contains elements that are not in this pathway:
* we just make sure any that are in this pathway are removed.
*/
public void remove(final Iterator<LWComponent> toRemove) {
removeEntries(toRemove, -1, false);
}
/**
* Using this single setEntries call with a variable key allows
* this to fully support both undo and redo for both add/remove while maintaining
* list order and current index position throughout.
*/
private void setEntries(final String keyName, final List<Entry> newEntries, int newIndex)
{
final List<Entry> oldEntries = mEntries;
mEntries = newEntries;
mSecureEntries = java.util.Collections.unmodifiableList(mEntries);
// Make sure we're only listening to the right folks, and that they know they're
// in this pathway. Nodes in the pathway more than once are fine, as the
// addLWCListener and addPathwayRef calls in ensureMemberRefs only have effect if
// they haven't already been added.
for (Entry e : oldEntries)
if (!newEntries.contains(e))
e.removeFromModel();
for (Entry e : newEntries)
e.ensureModel();
final int oldIndex = mCurrentIndex;
if (newIndex >= -1)
setIndex(newIndex);
//mCurrentIndex = newIndex;
else if (mCurrentIndex >= newEntries.size())
setIndex(newEntries.size() - 1);
//mCurrentIndex = newEntries.size() - 1;
// no matter what, make sure to broadcast the entry at
// the current index, as it's possible for the index
// to stay the same, but the entry change, as on a
// delete or an undo of a delete.
broadcastCurrentEntry();
notify(keyName, new Undoable() { void undo() {
setEntries(keyName, oldEntries, oldIndex);
}});
}
private void broadcastCurrentEntry() {
if (VUE.getActivePathway() == this) {
if (mCurrentIndex < 0) {
VUE.setActive(LWPathway.Entry.class, this, this.asEntry());
} else {
VUE.setActive(LWPathway.Entry.class, this, getEntry(mCurrentIndex));
}
}
}
public synchronized void LWCChanged(LWCEvent e)
{
if (e.key == LWKey.Deleting) {
removeAllOnDelete(e.getComponent());
} else {
// rebroadcast our child events so that the LWPathwayList which is
// listening to us can pass them on to the PathwayTableModel
mChangeSupport.dispatchEvent(e);
}
}
/**
* Remove all instances of @param deleted from this pathway
* Used when a component has been deleted.
*/
private void removeAllOnDelete(LWComponent deleted)
{
removeEntries(Util.iterable(deleted), -1, true);
}
public LWMap getMap(){
return (LWMap) getParent();
}
public void setMap(LWMap map) {
setParent(map);
ensureID(this);
}
public void setOpen(boolean open){
mOpen = open;
notify("pathway.open");
// Although this property is actually saved, it doesn't seem worthy of having
// it be in the undo list -- it's more of a GUI config.
}
public boolean isOpen() {
return mOpen;
}
public int length() {
return mEntries.size();
}
public void setFirst()
{
if (length() > 0)
setIndex(0);
}
/** @return true if selected is last item, or none selected */
public boolean atFirst(){
return mCurrentIndex <= 0;
}
/** @return true if selected is first item, or none selected */
public boolean atLast(){
return mCurrentIndex == -1 || mCurrentIndex == (length() - 1);
}
public void setLast() {
if (length() > 0)
setIndex(length() - 1);
}
public void setPrevious(){
if (mCurrentIndex > 0)
setIndex(mCurrentIndex - 1);
}
public void setNext(){
if (mCurrentIndex < (length() - 1))
setIndex(mCurrentIndex + 1);
}
/**
* Make sure we've completely cleaned up the pathway when it's
* been deleted (must get rid of LWComponent references to this
* pathway)
*/
@Override
protected void removeFromModel()
{
super.removeFromModel();
for (Entry e : mEntries)
e.removeFromModel();
}
@Override
protected void restoreToModel()
{
super.restoreToModel();
for (Entry e : mEntries)
e.ensureModel();
}
@Override public void preCacheContent() {
if (DEBUG.Enabled) Log.debug("preCacheContent: not currently enabled for " + getClass());
// for (Entry e : mEntries) { // cache sub-queue is FIFO, so handle front-to-back
// //Log.debug("PRE-CACHE-ENTRY " + e);
// preCacheEntryContent(e.getFocal());
// }
}
public MasterSlide getMasterSlide() {
if (mMasterSlide == null && !mXMLRestoreUnderway)
mMasterSlide = buildMasterSlide();
return mMasterSlide;
}
/** for persistance only */
public void setMasterSlide(MasterSlide slide) {
mMasterSlide = slide;
}
protected MasterSlide buildMasterSlide() {
if (DEBUG.PATHWAY) out("BUILDING MASTER SLIDE:\n");
return new MasterSlide(this);
}
// if we want to use a LWSlide subclass for the master slide, we'll need
// a lw_mapping entry that subclasses LWSlide so it will know what
// fields to save, instead of just dumping all of them.
// TODO: add master slide subclass to lw_mapping which needn't add anything over
// it's superclass, but it will let us save/restore instances of this that
// can do stuff like always return 0,0 x/y values.
private static void getMasterStyle() {
System.out.println("MASTER SLIDE CSS: " + VueResources.getURL("masterSlide.css"));
edu.tufts.vue.style.StyleReader.readStyles("masterSlide.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());
}
}
/**
* A special type of slide used for generating "virtual" slides for viewing
* a single on-map node in a slide-like presentation. These are not
* editable.
*/
static final class MapSlide extends LWSlide {
MapSlide(Entry e) {
setPathwayEntry(e);
//Util.printStackTrace("new MapSlide " + this + " for entry " + e);
disableProperty(LWKey.FillColor); // we don't persist map slides, so don't allow this to change: won't be permanent
disableProperty(LWKey.Label);
if (e.node instanceof LWContainer) {
setParent((LWContainer)e.node);
} else {
if (DEBUG.CONTAINMENT)
Util.printStackTrace(this + ";\n\tcan't yet parent this slide to non-container LWComponent:\n\t" + e.node
+ ";\n\t(This node cannot yet have an on-map slide icon.)");
else
Log.debug("can't yet create on-map slide icon for: " + e.node);
}
}
@Override
protected void drawChildren(DrawContext dc)
{
// must turn off clip-optimization: as we're drawing the source-node
// somewhere arbitrary, we must draw everything no matter what -- it or it's
// children can't check their bounds against the clip region
dc.setClipOptimized(false);
final LWComponent node = getSourceNode();
node.drawFit(dc, getZeroBounds(), node.getFocalMargin());
}
/** @return true -- MapSlides always have content (even tho no children) */
public boolean hasContent() {
return true;
}
/** @return null */
@Override
public Object getTypeToken() {
return null;
}
/** @return false */
public boolean supportsChildren() {
return false;
}
/** @return false: map slides have nothing to sync */
@Override
public boolean canSync() {
return false;
}
@Override
public String getLabel() {
return "View of " + getEntry().node.getDisplayLabel() + " in " + getEntry().pathway.getDisplayLabel();
}
@Override
public String getComponentTypeLabel() {
return "NodeView";
}
@Override
public boolean hasPicks() { return false; }
@Override
public boolean hasChildren() { return false; }
@Override
public int numChildren() { return 0; }
@Override
public List<LWComponent> getPickList(PickContext pc, List<LWComponent> stored) { return stored; }
@Override
public java.util.List<LWComponent> getChildList() { return java.util.Collections.EMPTY_LIST; }
@Override
public java.util.List<LWComponent> getChildren() { return java.util.Collections.EMPTY_LIST; }
@Override
protected LWComponent pickChild(PickContext pc, LWComponent c) { return this; }
@Override
public LWSlide duplicate(CopyContext cc)
{
Util.printStackTrace("MapSlide: illegal duplicate " + this);
return null;
}
}
// // we don't support standard children: we shouldn't be calling any of these
// @Override
// public void addChildren(Iterable i) {
// Util.printStackTrace("Unsupported: LWPathway.addChildren in " + this);
// }
@Override
protected void addChildImpl(LWComponent c, Object context) { throw new UnsupportedOperationException("add:" +context + "; " + c); }
@Override
public void removeChildren(Iterable i, Object context) {
Util.printStackTrace("Unsupported: LWPathway.removeChildren in " + this);
}
@Override
protected void setScale(double scale) {}
/**
* for persistance: override of LWContainer: pathways never save their children
* as they don't own them -- they only save ID references to them. Pathways
* are only "virtual" containers, not proper parents of their children.
*/
@Override
public java.util.List<LWComponent> getChildList() {
if (DEBUG.XML || DEBUG.PATHWAY) out("getChildList returning EMPTY, as always");
return java.util.Collections.EMPTY_LIST;
}
/** @return Collections.EMPTY_LIST -- the children of pathways are non-proper, and can't be accessed this way */
@Override
public java.util.List<LWComponent> getChildren() {
if (DEBUG.XML || DEBUG.PATHWAY) out("getChildren returning EMPTY, as always");
return Collections.EMPTY_LIST;
}
/** hide children from hierarchy as per getChildList */
@Override
public Iterator<LWComponent> getChildIterator() {
return VueUtil.EmptyIterator;
}
/** return a read-only list of our Entries */
public java.util.List<Entry> getEntries() {
return mSecureEntries;
}
/** for castor only -- it needs to modify (build up) the list during restore */
public java.util.List<Entry> getPersistEntries() {
return mEntries;
}
/** changes in semantics from LWComponent: count entries IN this pathway, not pathways we're a member of */
@Override
public boolean hasEntries() {
return mEntries.size() > 0;
}
@Override
public LWComponent getChild(int index) {
throw new UnsupportedOperationException("pathways don't have proper children");
}
@Override
protected final void addEntryRef(LWPathway.Entry e) {
Util.printStackTrace(this + " illegal addEntryRef " + e);
}
@Override
protected final void removeEntryRef(LWPathway.Entry e) {
Util.printStackTrace(this + " illegal removeEntryRef " + e);
}
// /* for castor only -- apparently castor's claim to implement this type of access to collections is bogus
// public Iterator iterateEntries() {
// return mEntries;
// }
// public void castorAddEntry(Entry e) {
// out("CASTOR ADD ENTRY: " + e);
// }
@Override
public Collection<LWComponent> getAllDescendents(final ChildKind kind, final Collection bag, Order order)
{
if (kind == ChildKind.ANY) {
if (mMasterSlide != null) {
if (order == Order.TREE) {
bag.add(mMasterSlide);
mMasterSlide.getAllDescendents(kind, bag, order);
} else {
// Order.DEPTH
mMasterSlide.getAllDescendents(kind, bag, order);
bag.add(mMasterSlide);
}
}
for (Entry e : mEntries) {
if (!e.isMapView() && e.slide != null) {
if (order == Order.TREE) {
bag.add(e.slide);
e.slide.getAllDescendents(kind, bag, order);
} else {
e.slide.getAllDescendents(kind, bag, order);
bag.add(e.slide);
// Order.DEPTH
}
}
}
}
return bag;
}
void completeXMLRestore(LWMap map)
{
if (DEBUG.INIT || DEBUG.IO || DEBUG.XML) Log.debug(this + " completeXMLRestore, map=" + map);
setParent(map);
if (mMasterSlide != null) {
mMasterSlide.setParent(this);
mMasterSlide.completeXMLRestore();
}
// Replace the incomplete entries (castor can only use the default constructor) with fully restored entries
List<Entry> newEntries = new java.util.ArrayList(mEntries.size());
for (Entry e : mEntries)
newEntries.add(new Entry(this, e));
mEntries.clear(); // clear for GC, and setEntries needs no old value at init
// This is for backward compat with older save files where
// Pathway elements were stored only by ID
if (mOldStyleMemberIDList.size() > 0) {
final Collection<LWComponent> allRestored = map.getAllDescendents(ChildKind.ANY);
for (String id : mOldStyleMemberIDList) {
LWComponent c = map.findByID(allRestored, id);
if (DEBUG.XML || DEBUG.PATHWAY) out("RESTORING old-style path element " + c);
newEntries.add(new Entry(this, c));
}
}
// setEntries will ensure all of our model pointersd are correctly maintained
setEntries("pathway.restore", newEntries, 0);
// // [2007-11-05 -- hasn't been true for a while: slides are parented to their nodes as "slide-icons"]
// // The parent of a slide tied to an Entry is the LWPathway itself
// for (Entry e : mEntries) {
// if (e.slide != null) {
// e.slide.setParent(this);
// //e.slide.setSourceNode(e.node);
// }
// }
// Now restore old-style notes
for (LWPathwayElementProperty pep : mOldStyleProperties) {
for (Entry e : findEntriesWithNodeID(pep.getElementID())) {
if (DEBUG.XML || DEBUG.PATHWAY) out("RESTORING old style notes for " + e);
e.setNotes(pep.getElementNotes());
}
}
updateMemberVisibility(isVisible());
if (DEBUG.XML || DEBUG.PATHWAY) out("RESTORED");
mXMLRestoreUnderway = false;
}
/** in support of restoring old-style pathway XML */
private List<Entry> findEntriesWithNodeID(String ID)
{
List<Entry> matches = new java.util.ArrayList();
for (Entry e : mEntries)
if (e.node != null && e.node.getID().equals(ID))
matches.add(e);
return matches;
}
/** for persistance: XML save/restore only for old-style (non Entry) pathway code */
public java.util.List<String> getElementIDList() {
if (DEBUG.XML || DEBUG.PATHWAY) out("getElementIDList: " + mOldStyleMemberIDList);
if (mXMLRestoreUnderway)
return mOldStyleMemberIDList;
else
return null; // we no longer save this way: only read in
}
/** for persistance: XML save/restore only for old-style (non Entry) pathway code */
public java.util.List getElementPropertyList()
{
if (mXMLRestoreUnderway)
return mOldStyleProperties;
else
return null; // we no longer save this way: only read in
}
private static final boolean PathwayAsDots = true;
private static final int ConnectorStrokeWidth = VueResources.getInt("pathway.stroke.width", 5);
public static final int PathBorderStrokeWidth = 9;
// forcing this up no matter what ensure our paint clipping will always work -- can optimize later
//public static final int BorderStrokeWidth = PathwayAsDots ? 0 : 8; // technically, this is half the stroke, but it's the visible stroke
private static final float[] DashPattern = { 8, 6 };
//private static final float[] SelectedDash = { 4, 4 };
//private static final float[] MultiSelectedDash = { 8, 8 };
private static final float DotSize = VueResources.getInt("pathway.dotSize", 20);
private static final float DotRadius = DotSize / 2;
//private static final BasicStroke ConnectorStroke = new BasicStroke();
private static final BasicStroke ConnectorStroke
= new BasicStroke(ConnectorStrokeWidth,
BasicStroke.CAP_ROUND,
//PathwayAsDots ? BasicStroke.CAP_ROUND : BasicStroke.CAP_BUTT,
BasicStroke.JOIN_BEVEL, // ignored: always straight (no bends)
0f, // ignored (mitre-limit)
DashPattern,
0f // dash-phase
);
private static final BasicStroke ConnectorStrokePlain
= new BasicStroke(ConnectorStrokeWidth,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_BEVEL);
private static final BasicStroke ConnectorStrokeActive
= new BasicStroke(10,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_BEVEL, // ignored: always straight (no bends)
0f, // ignored (mitre-limit)
new float[] {8, 12},
//null,
0f // dash-phase
);
private static final BasicStroke PathBorderStroke
= new BasicStroke(PathBorderStrokeWidth,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
private static final BasicStroke PathBorderStrokeDashed
= new BasicStroke(PathBorderStrokeWidth,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_ROUND,
10f, // mitre-limit
new float[] { 8, 8 },
0f
);
private static final BasicStroke PathBorderStrokeDashed2
= new BasicStroke(PathBorderStrokeWidth,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_ROUND,
10f, // mitre-limit
new float[] { 8, 8 },
8f
);
/** @return the color of the pathway (same as stroke-color) */
public Color getColor() {
return mStrokeColor.get();
}
public static void decorateOver(final LWComponent node, final DrawContext dc)
{
if (PathwayAsDots || node instanceof LWLink)
drawPathwayDot(node, dc);
}
public static void decorateUnder(final LWComponent node, final DrawContext dc)
{
if (PathwayAsDots)
return;
if (node instanceof LWLink || node instanceof LWPortal || node instanceof LWGroup)
return; // do nothing: these decorate via fill and/or dot, not border
drawPathwayBorder(node, dc);
// Only draw ONE pathway border...
// if (true||node.isTransparent()) {
// for (LWPathway pathway : node.getPathways()) {
// //if (!dc.isFocused && path.isDrawn()) {
// if (pathway.isDrawn())
// pathway.drawPathwayBorder(node, dc.create());
// }
// }
}
/** for drawing just before the component draw's itself -- this is a draw-under,
* and we're already at the zero transform for the component
*/
private static void drawPathwayBorder(LWComponent node, DrawContext dc)
{
// we're already transformed into the local GC -- just draw the raw shape
final Color c = node.getPriorityPathwayColor(dc);
if (c == null) {
// int count = 0;
// for (LWPathway p : node.getPathways()) {
// if (p.isDrawn()) {
// if (count > 0)
// dc.g.setStroke(PathBorderStrokeDashed2);
// else
// dc.g.setStroke(PathBorderStrokeDashed);
// dc.g.setColor(p.getColor());
// dc.g.draw(node.getZeroShape());
// count++;
// }
// }
return;
}
//final Color c = node.getPriorityPathwayColor(dc);
dc.g.setColor(c);
//dc.g.setColor(getColor());
//final int strokeWidth = PathBorderStrokeWidth + node.getStrokeWidth();
//dc.g.setStroke(PathBorderStroke);
dc.g.setStroke(new BasicStroke(PathBorderStrokeWidth + node.getStrokeWidth(),
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND));
dc.g.draw(node.getZeroShape());
}
private static void drawPathwayDot(LWComponent node, DrawContext dc)
{
// VUE-892 fix shows dots still drawing even when node is filtered
if(node.isFiltered())
return;
LWPathway onlyPathway = null;
int visiblePathMemberships = 0;
final LWPathway activePathway = VUE.getActivePathway(); // todo: from draw context
for (LWPathway p : node.getPathways()) {
if (p.isDrawn()) {
onlyPathway = p;
visiblePathMemberships++;
// if (p == activePathway) {
// // override for active-pathway:
// visiblePathMemberships = 1;
// break;
// } else {
// visiblePathMemberships++;
// }
}
}
float x, y;
if (node instanceof LWLink) {
x = node.getZeroCenterX();
y = node.getZeroCenterY();
} else {
final Point2D corner = node.getZeroNorthWestCorner();
x = (float) corner.getX();
y = (float) corner.getY();
// Enable this to move the dot internal to the node if it's inside another node: prevent dot from obscuring neighbors.
// (Will need to update the conncetor code also: merge this into one routine and base the conncetor code on the zero result)
// if (node.getParent().getTypeToken() == LWNode.class) {
// // a node inside an auto-layout node is messy: put the dot on top of it
// // checking the type token ensures it's a real LWNode, not a subclass
// x += DotRadius * node.getScaleF();
// y += DotRadius * node.getScaleF();
// }
}
//dc.g.setComposite(PathTranslucence);
boolean filledEntirely = false;
// // DRAW AN OVERLAY
// if (onlyPathway != null &&
// (node instanceof LWGroup || node instanceof LWImage)) {
// final Color fill = node.getPriorityPathwayColor(dc);
// if (fill != null) {
// final Rectangle2D.Float bounds = node.getZeroBounds();
// //if (!c.isTransparent())
// grow(bounds, PathBorderStrokeWidth);
// //dc.g.setColor(onlyPathway.getStrokeColor());
// //dc.g.setColor(onlyPathway.getColor()); // TODO: use same logic is LWPortal to get fill color...
// //dc.g.setColor(node.getPriorityPathwayColor(dc));
// dc.g.setColor(fill);
// dc.g.fill(bounds);
// }
// }
if (visiblePathMemberships > 1) {
final Arc2D.Float arc = new Arc2D.Float(x - DotRadius,
y - DotRadius,
DotSize,
DotSize,
0, 0, Arc2D.PIE);
final float pieSlice = 360 / visiblePathMemberships;
int i = 0;
dc.g.setStroke(STROKE_HALF);
for (LWPathway p : node.getPathways()) {
if (!p.isDrawn())
continue;
dc.g.setColor(p.getColor());
arc.setAngleStart(90 + i * pieSlice);
arc.setAngleExtent(pieSlice);
dc.g.fill(arc);
//dc.g.setColor(Color.gray);
//dc.g.draw(arc);
i++;
}
} else if (onlyPathway != null && !filledEntirely) {
dc.g.setColor(onlyPathway.getStrokeColor());
final RectangularShape dot = new java.awt.geom.Ellipse2D.Float(0,0, DotSize,DotSize);
dot.setFrameFromCenter(x, y, x+DotRadius, y+DotRadius);
dc.g.fill(dot);
}
}
private static final boolean HEAD_END = true;
private static final boolean TAIL_END = false;
private static void setConnectionPoint(Line2D.Float line, boolean end, LWComponent c) {
final float x, y;
if (c instanceof LWLink) {
x = c.getMapCenterX();
y = c.getMapCenterY();
} else {
final Point2D corner = c.getZeroNorthWestCorner();
x = (float) (c.getMapXPrecise() + corner.getX() * c.getScale());
y = (float) (c.getMapYPrecise() + corner.getY() * c.getScale());
}
if (end == HEAD_END) {
line.x1 = x;
line.y1 = y;
} else {
line.x2 = x;
line.y2 = y;
}
}
/** used for re-drawing an entire pathway with it's dots -- e.g., for hilighting it */
public void drawPathwayWithDots(DrawContext dc) {
drawPathway(dc.push()); dc.pop();
for (Entry e : getEntries()) {
if (e.node != null) {
DrawContext ndc = dc.push();
e.node.transformZero(ndc.g);
decorateOver(e.node, ndc);
dc.pop();
}
}
}
public void drawPathway(DrawContext dc)
{
final Line2D.Float connector = new Line2D.Float();
if (VUE.getActivePathway() == this) {
dc.g.setStroke(ConnectorStroke);
dc.g.setColor(getColor());
} else {
dc.g.setStroke(ConnectorStrokePlain);
dc.g.setColor(getColor());
}
double alpha = VUE.getInteractionToolsPanel().getAlpha();
if (alpha != 1) {
// "Fade" this pathway.
dc.setAlpha(alpha);
}
// if (dc.isPresenting()) {
// if (VUE.getActivePathway() == this) {
// dc.g.setStroke(ConnectorStrokeActive);
// //dc.g.setColor(Util.alphaMix(getColor(), Color.gray));
// dc.g.setColor(getColor());
// } else {
// dc.g.setStroke(ConnectorStrokePlain);
// dc.g.setColor(getColor());
// }
// } else {
// dc.g.setStroke(ConnectorStroke);
// dc.g.setColor(getColor());
// }
LWComponent last = null;
for (Entry e : mEntries) {
if (e.node == null)
continue;
final LWComponent next = e.node;
if (last != null && last.isDrawn() && next.isDrawn()) {
//if (PathwayAsDots || c instanceof LWLink || last instanceof LWLink) {
if (PathwayAsDots) {
setConnectionPoint(connector, HEAD_END, last);
setConnectionPoint(connector, TAIL_END, next);
// todo: we need to scale the clip for the dot scale, tho this will under-clip
// the non-scaled end...
VueUtil.clipEnds(connector, DotRadius * Math.min(last.getMapScale(), next.getMapScale()));
} else {
VueUtil.computeConnector(last, next, connector);
}
// if (dc.isPresenting() && dc.getAlpha() != 1f && VUE.getActivePathway() == this) {
// DrawContext _dc = dc.create();
// Log.debug("REVERTING ALPHA");
// _dc.setAlpha(1);
// _dc.g.draw(connector);
// _dc.dispose();
// } else {
// dc.g.draw(connector);
// }
dc.g.draw(connector);
if (DEBUG.BOXES) {
Ellipse2D dot = new Ellipse2D.Float(0,0, 10,10);
Point2D.Float corner = (Point2D.Float) connector.getP1();
corner.x+=5; corner.y+=5;
dot.setFrameFromCenter(connector.getP1(), corner);
dc.g.setColor(Color.green);
dc.g.fill(dot);
corner = (Point2D.Float) connector.getP2();
corner.x+=5; corner.y+=5;
dot.setFrameFromCenter(connector.getP2(), corner);
dc.g.setColor(Color.red);
dc.g.fill(dot);
dc.g.setColor(getStrokeColor());
}
}
last = next;
}
}
/*
if (DEBUG.PATHWAY) {
if (dc.getIndex() % 2 == 0)
dash_phase = 0;
else
dash_phase = 0.5f;
}
if (DEBUG.PATHWAY&&DEBUG.BOXES) System.out.println("Drawing " + this + " index=" + dc.getIndex() + " phase=" + dash_phase);
*/
/*
public void drawComponentDecorations(DrawContext dc, LWComponent c)
{
//boolean selected = (getCurrentNode() == c && VUE.getActivePathway() == this);
final boolean selected = false; // turn off the "marching ants" -- don't show the current item on the pathway
if (selected)
dc.g.setComposite(PathSelectedTranslucence);
else
dc.g.setComposite(PathTranslucence);
dc.g.setColor(getStrokeColor());
if (PathwayAsDots || c instanceof LWLink) {
int visiblePathMemberships = 0;
for (LWPathway p : c.getPathways())
if (p.isVisible())
visiblePathMemberships++;
final float x = c.getZeroCenterX();
final float y = c.getZeroCenterY();
if (visiblePathMemberships > 1) {
final Arc2D.Float arc = new Arc2D.Float(x - DotRadius,
y - DotRadius,
DotSize,
DotSize,
0, 0, Arc2D.PIE);
final float pieSlice = 360 / visiblePathMemberships;
int i = 0;
for (LWPathway p : c.getPathways()) {
if (!p.isVisible())
continue;
dc.g.setColor(p.getStrokeColor());
arc.setAngleStart(90 + i * pieSlice);
arc.setAngleExtent(pieSlice);
dc.g.fill(arc);
i++;
}
} else {
final RectangularShape dot = new java.awt.geom.Ellipse2D.Float(0,0, DotSize,DotSize);
dot.setFrameFromCenter(x, y, x+DotRadius, y+DotRadius);
dc.g.fill(dot);
}
if (!c.isTransparent())
return;
}
int strokeWidth = BorderStrokeWidth;
//dc = new DrawContext(dc);
// because we're drawing under the object, only half of the
// amount we add to to the stroke width is visible outside the
// edge of the object, except for links, which are
// one-dimensional, so we use a narrower stroke width for
// them.
//if (c instanceof LWLink)
// strokeWidth /= 2;
strokeWidth += c.getStrokeWidth();
if (selected) {
//if (DEBUG.PATHWAY && dc.getIndex() % 2 != 0) dash_phase = c.getStrokeWidth();
int visiblePathMemberships = 0;
for (LWPathway p : c.getPathways())
if (p.isVisible())
visiblePathMemberships++;
//boolean offsetDash =
dc.g.setStroke(new BasicStroke(strokeWidth
, BasicStroke.CAP_BUTT
, BasicStroke.JOIN_BEVEL
, 0f
, visiblePathMemberships > 1 ? MultiSelectedDash : SelectedDash
, 0
//, offsetDash ? 8 : 0
));
} else {
dc.g.setStroke(new BasicStroke(strokeWidth));
}
// we're already transformed into the local GC -- just draw the raw shape
dc.g.draw(c.getZeroShape());
dc.g.setComposite(AlphaComposite.Src);//TODO: restore old composite
}
*/
public String toString()
{
return "LWPathway[" + getID()
+ " " + label
+ " n="
//+ (children==null?-1:children.size())
+ mEntries.size()
+ " i="+mCurrentIndex
+ " " + (getMap()==null?"null":getMap().getLabel())
+ "]";
}
/** constructor used for un-marshalling only */
public LWPathway() {
initPathway();
}
}
/*
protected MasterSlide buildMasterSlide() {
out("BUILDING MASTER SLIDE:\n");
//final LWSlide m = LWSlide.create();
//m.setStrokeWidth(0);
//m.setFillColor(Color.darkGray);
//m.setLabel("Master Slide on Pathway: " + getLabel());
//m.setNotes("This is the Master Slide for Pathway \"" + getLabel() + "\"");
//m.setFillColor(getStrokeColor());
//LWComponent titleText = NodeTool.createTextNode("Title Text");
//LWComponent itemText = NodeTool.createTextNode("Item Text");
//LWComponent footer = NodeTool.createTextNode(getLabel());
//m.setParent(LWPathway.this); // must set parent before ensureID will work
//ensureID(m);
//m.addChild(footer);
// Move the footer to lower right
//LWSelection s = new LWSelection(footer);
//Actions.AlignRightEdges.act(s);
//Actions.AlignBottomEdges.act(s);
//m.addChild(titleText);
//m.addChild(itemText);
//return m;
}
*/
/*
* A bit of a hack, but works well: any LWComponent can serveLW
* as a style holder, and with special notification code, will
* updating those who point back to us when it changes.
* Also, making a LWNode allows it to also be an editable
* node for full node styling, or a "text" node for text styling.
*/
// Will not persist yet...
// TODO: consider: do NOT have a separate style object: any LWComponent can
// be tagged as being a "masterStyle", and if so, it will invoke
// the below broadcasting code...
/*
public static class NodeStyle extends LWNode {
NodeStyle() {}
NodeStyle(String label) {
super(label);
}
// Note that would could also do this in MasterSlide.broadcastChildEvent,
// by checking if e.source is an LWStyle object. We might want
// to do this if we end up with a bunch of different LWStyle
// classes (e.g. TextStyle, LinkStyle, etc)
protected synchronized void notifyLWCListeners(LWCEvent e) {
super.notifyLWCListeners(e);
if ((e.key instanceof Key) == false || getParent() == null) {
// This only works with real Key's, and if parent is null,
// we're still initializing.
return;
}
final Key key = (Key) e.key;
if (key.isStyleProperty) {
// 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.
out("STYLE OBJECT UPDATING STYLED CHILDREN with " + key);
//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 c : getMap().getAllDescendents(ChildKind.ANY)) {
if (c.mParentStyle == this && c != this) { // we should never be point back to ourself, but just in case
// Only copy over the style value if was previously set to our existing style value
try {
if (key.valueEquals(c, e.getOldValue()))
key.copyValue(this, c);
} catch (Throwable t) {
tufts.Util.printStackTrace(t, "Failed to copy value from " + e + " old=" + e.oldValue);
}
}
}
}
}
}
*/