/*
* 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.gui.GUI;
import tufts.vue.NodeTool.NodeModeTool;
import java.util.*;
import java.awt.Color;
import java.awt.Composite;
import java.awt.BasicStroke;
import java.awt.geom.*;
/**
*
* Container for displaying slides.
*
* @author Scott Fraize
* @version $Revision: 1.118 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $
*/
public class LWSlide extends LWContainer
{
protected static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWSlide.class);
public static final int SlideWidth = 800;
public static final int SlideHeight = 600;
// 640/480 == 1024/768 == 1.333...
// Keynote uses 800/600 (1.3)
// PowerPoint defaut 720/540 (1.3) Based on 72dpi? (it has a DPI option)
public static final float SlideAspect = ((float)SlideWidth) / ((float)SlideHeight);
protected static final int SlideMargin = 30;
private transient LWPathway.Entry mEntry;
/** public only for persistance */
public LWSlide() {
disableProperty(LWKey.Label);
disablePropertyTypes(KeyType.STYLE);
enableProperty(LWKey.FillColor);
takeSize(SlideWidth, SlideHeight);
takeScale(LWComponent.SlideIconScale);
}
/** set a runtime entry marker for this slide for use in presentation navigation */
protected void setPathwayEntry(LWPathway.Entry e) {
mEntry = e;
}
/** get the LWPathway.Entry for this slide if it is a pathway slide, otherwise null */
LWPathway.Entry getPathwayEntry() {
return mEntry;
}
public LWPathway.Entry getEntry() {
return mEntry;
}
/** always just return child list -- slides never have extra picks (e.g., slide icons) */
@Override
public List<LWComponent> getPickList(PickContext pc, List<LWComponent> stored) {
return (List) getChildren();
}
@Override
public void setNotes(String s) {
if (mEntry == null) {
super.setNotes(s);
} else {
mEntry.setNotes(s);
// may need to trigger an event so any listeners will know if this updated, tho
// current only a single notes panel has access to this, so we can skip it.
}
}
@Override
public String getNotes() {
if (mEntry == null) {
return super.getNotes();
} else {
return mEntry.getNotes();
}
}
/** @return false -- slides not currently allowed to have a resource */
@Override
public boolean hasResource() {
return false;
}
/** do nothing */
@Override
public void setResource(Resource r) {}
/** @return null -- sides currently not allowed to have a resource */
@Override
public Resource getResource() {
return null;
}
@Override
public boolean supportsUserResize() {
return isMoveable() && DEBUG.Enabled;
}
/** @return false: slides can't be selected with anything else */
public boolean supportsMultiSelection() {
return isMoveable();
}
/** @return false: slides can never have slides */
@Override
public final boolean supportsSlide() {
return false;
}
/** @return true: slides never have slide-icon entries of their own */
@Override
public final boolean hasEntries() {
return false;
}
@Override
public final boolean fullyContainsChildren() {
return true;
}
@Override
public boolean isVisible() {
if (mEntry != null)
return super.isVisible() && mEntry.pathway.isShowingSlides();
else
return super.isVisible();
}
/** @return true only if we're not a pathway generated slide */
public boolean supportsReparenting() {
if (!super.supportsReparenting())
return false;
else
return mEntry == null;
}
@Override
public boolean isPathwayOwned() {
return mEntry != null;
}
@Override
public boolean isMoveable() {
return isPathwayOwned() == false;
}
/** @return false */
@Override
public boolean canLinkToImpl(LWComponent target) {
return isMoveable();
}
/** slides never considered translucent: they're not on the map needing backfill when they're the focal */
@Override
public boolean isTranslucent() {
return false;
}
// todo: won't be able to use this when we allow variable sized slides...
private static final Rectangle2D SlideZeroShape = new Rectangle2D.Float(0, 0, SlideWidth, SlideHeight);
@Override
public final java.awt.Shape getZeroShape() {
if (mEntry == null)
return super.getZeroShape(); // if entry is null, is an on-map slide that may have been resized
else
return SlideZeroShape; // as slides all same size for now, can use a constant zeroShape
}
// /** @return false -- slides themseleves never have slide icons: only nodes that own them */
// @Override
// public final boolean isDrawingSlideIcon() {
// return false;
// }
// @Override
// protected final void addEntryRef(LWPathway.Entry e) {
// Util.printStackTrace(this + " slides can't addEntryRef " + e);
// }
// @Override
// protected final void removeEntryRef(LWPathway.Entry e) {
// Util.printStackTrace(this + " slides can't removeEntryRef " + e);
// }
@Override
public int getFocalMargin() {
return 0;
}
@Override
protected void setParent(LWContainer parent) {
super.setParent(parent);
if (mEntry == null && parent instanceof LWPathway == false) { // parent will be pathway for master slides
// if no longer a slide icon (is directly part of a map), set a fill if we didn't have one
if (getFillColor() == null)
setFillColor(Color.gray);
takeScale(0.5);
enableProperty(LWKey.Label);
}
// if (parent instanceof LWPathway) {
// ; // default
// } else {
// // This should only ever happen if a slide is drag-copied onto the map
// if (getFillColor() == null)
// setFillColor(Color.black);
// }
}
public LWComponent getSourceNode() {
return mEntry == null ? null : mEntry.node;
}
@Override
public String getLabel() {
if (supportsProperty(LWKey.Label))
return super.getLabel();
if (mEntry != null) {
if (getSourceNode() == null)
return "Slide in " + mEntry.pathway.getDisplayLabel();
else
return "Slide for " + getSourceNode().getDisplayLabel() + " in " + mEntry.pathway.getDisplayLabel();
} else
return "<LWSlide:initializing>"; // true during persist restore
}
/** create a default LWSlide */
public static LWSlide instance()
{
final LWSlide s = new LWSlide();
//s.setFillColor(new Color(0,0,0,64));
s.setFillColor(Color.black);
s.setStrokeWidth(0);
//s.setStrokeColor(Color.black);
s.setSize(SlideWidth, SlideHeight);
//setAspect(((float)GUI.GScreenWidth) / ((float)GUI.GScreenHeight));
s.setAspect(SlideAspect);
return s;
}
public boolean canSync() {
return mEntry != null && !mEntry.isMapView();
}
//public void rebuild() {}
public void preCacheContent() {
preCacheDescendents(this);
}
public void revertToMasterStyle() {
//out("REVERTING TO MASTER STYLE");
for (LWComponent c : getAllDescendents()) {
LWComponent style = c.getStyle();
if (style != null) {
c.copyStyle(style);
} else {
// TODO: shouldn't really need this, but if anything gets detached from it's
// style, this should re-attach it, tho we need a bit in the node
// to know if we never want to do this: e.g. we always want a node
// to stay "regular" node on the slide.
applyMasterStyle(c, true);
}
}
setFillColor(null); // this is how we revert a slide's bg color to that of the master slide
}
private static abstract class SlideStylingTraversal extends LWTraversal {
SlideStylingTraversal() {
super(null);
}
@Override
public boolean acceptChildren(LWComponent c) {
return c instanceof LWGroup == false && c.hasChildren();
}
@Override
public boolean acceptTraversal(LWComponent c) {
return true;
}
@Override
public boolean accept(LWComponent c) {
return c instanceof LWGroup == false && c instanceof LWSlide == false;
}
abstract public void visit(LWComponent c);
// @Override
// public void visit(LWComponent c) {
// }
}
@Override
public void XML_completed(Object context) {
// We shouldn't need this anymore now that we persist slide style,
// tho we're leaving it in for at least a while to catch recently
// made presentations -- SMF 2007-11-16
new SlideStylingTraversal() {
public void visit(LWComponent c) {
c.setFlag(Flag.SLIDE_STYLE);
//out("slide bit: " + c);
}
}.traverse(this);
super.XML_completed(context);
}
private void applyMasterStyle(LWComponent node, final boolean resetStyle) {
final MasterSlide master = getMasterSlide();
new SlideStylingTraversal() {
public void visit(LWComponent c) {
applyMasterStyle(master, c, resetStyle);
}
}.traverse(node);
}
/**
* SMF 2009-10-15: keep existing semantics for now, and always fully reset
* the style of any object added to a slide, even if it was duplicated
* from the same slide and it had already diverged from the master style.
* This can run counter to user expectation when duplicating an object
* on a slide (or cut/pasting from a slide in the same pathway), as
* it can look unexpectedly different.
*/
private static boolean KEEP_DIVERGENT_STYLES_ON_DUPLICATE = false;
private static void applyMasterStyle(MasterSlide master, LWComponent c, boolean resetStyle) {
if (master == null) {
Log.error("NULL MASTER SLIDE: can't apply master style to " + c);
return;
}
c.setFlag(Flag.SLIDE_STYLE);
c.mAlignment.set(Alignment.LEFT);
final LWComponent style;
if (c.hasResource() && !c.getResource().isImage())
style = master.getLinkStyle();
else if (c instanceof LWText)
style = master.getTextStyle();
else if (c instanceof LWNode)
style = master.getTextStyle();
//else if (c instanceof LWPortal)
//style = null;
else
style = null;
if (style != null) {
if (KEEP_DIVERGENT_STYLES_ON_DUPLICATE) {
if (resetStyle)
c.setStyle(style);
else
c.takeStyle(style);
} else {
c.setStyle(style);
}
}
if (DEBUG.Enabled) {
if (c.getStyle() == null)
track("skip-style", String.format("%-14s %s",
"(" + (c.getTypeToken() instanceof Class ? ((Class)c.getTypeToken()).getSimpleName() : c.getTypeToken()) + ")",
c));
else
track("styled-as", String.format("%-14s %s", c.getStyle().getLabel()+";", c));
}
}
void applyStyle(LWComponent c) {
applyStyle(c, true);
}
private void applyStyle(LWComponent c, boolean resetStyle) {
//if (DEBUG.PRESENT || DEBUG.STYLE)
//track("styling", c + "; curStyle=" + c.getStyle());
c.setFlag(Flag.SLIDE_STYLE);
if (c.getStyle() == null)
applyMasterStyle(c, resetStyle);
// if (LWNode.isImageNode(c))
// c.mAlignment.set(Alignment.RIGHT);
}
private static void track(String where, Object o) {
if (DEBUG.Enabled)
Log.debug(String.format("%16s: %s",
where,
o instanceof LWComponent ? o : (o instanceof String ? o : Util.tags(o))));
}
@Override
protected void addChildImpl(LWComponent c, Object context)
{
track("addChildImpl/"+context, c);
if (context == ADD_PASTE || context == ADD_DROP) {
final LWContainer oldParent = c.getClientData(LWKey.OLD_PARENT);
if (oldParent instanceof LWSlide) {
if (((LWSlide)oldParent).getEntry().pathway == getEntry().pathway) {
// if this component was from any other slide on this same pathway,
// don't override any master-style divergent properties the user may
// have applied to the item is case this add is the result of a duplicate/cut/paste
applyStyle(c, false);
} else {
applyStyle(c);
}
} else {
applyStyle(c);
// Below now handled in LWImage.guessAtBestSize
// TODO: need a size request for LWImage, as the image itself
// may not be loaded yet (or just auto-handle this in userSetSize,
// or setSize or something.
// if (c instanceof LWImage) {
// //((LWImage)c).userSetSize(SlideWidth / 4, SlideWidth / 4, null);
// ((LWImage)c).setTmpSize(SlideWidth / 4, SlideWidth / 4); // not good enough -- no longer auto-sizes to old aspects
// track("resized", c);
// // todo: we actually want this to happen after we're sure we know the image's aspect,
// // which we won't if it's slowly loading -- perhaps we can handle this via a cleanup task.
// }
}
}
super.addChildImpl(c, context);
}
// /** Return true if our parent is the given pathway (as slides are currently owned by the pathway).
// * If our parent is NOT a pathway, use the default impl.
// */
@Override
public boolean inPathway(LWPathway p)
{
if (mEntry == null)
return super.inPathway(p);
else
return mEntry.pathway == p;
// if (getParent() instanceof LWPathway)
// return getParent() == p;
// else
// return super.inPathway(p);
}
public MasterSlide getMasterSlide() {
if (mEntry == null)
return null;
else
return mEntry.pathway.getMasterSlide();
}
@Override
public boolean isPresentationContext() {
return true;
}
/** implemented to return the bg color of the master slide (for proper on-slide text edit fill color) */
@Override
public Color getRenderFillColor(DrawContext dc) {
if (mFillColor.isTransparent()) {
final LWSlide master = getMasterSlide();
if (master == null)
return getFillColor();
else
return master.getFillColor();
} else
return getFillColor();
}
@Override
public Color getFinalFillColor(DrawContext dc) {
Color c = getRenderFillColor(dc);
return c == null ? super.getFinalFillColor(dc) : c;
}
public boolean isSlideIcon() {
return mEntry != null;
}
@Override
protected void drawImpl(DrawContext dc)
{
boolean drewBorder = false;
final boolean onMapSlideIcon = isSlideIcon() && (dc.focal != this || dc.focused == this);
if (onMapSlideIcon && dc.drawPathways()) {
// we have an entry: draw a pathway hilite
if (dc.isPresenting() || getEntry() == VUE.getActiveEntry()) {
dc.g.setColor(getEntry().pathway.getColor());
dc.g.setStroke(SlideIconPathwayStroke);
dc.g.draw(getZeroShape());
drewBorder = true;
}
} else if (dc.focal != this) {
if (isSelected() && dc.isInteractive()) {
// for on-map slides only: draw regular selection border if selection
dc.g.setColor(COLOR_HIGHLIGHT);
dc.setAbsoluteStroke(getStrokeWidth() + SelectionStrokeWidth);
dc.g.draw(getZeroShape());
drewBorder = true;
}
}
final LWSlide master = getMasterSlide();
final Color fillColor = getRenderFillColor(dc);
if (fillColor == null) {
if (DEBUG.Enabled) Util.printStackTrace("null fill " + this);
}
// else if (!dc.isClipOptimized()) {
// // if we're doing a zoomed-rollover of a slide, and the slide fill happens
// // to be the same as the map fill, (e.g., white), this will have no effect,
// // and the zoomed slide will zoom up with no fill (see-through). This is a
// // bit of a hack in that we're depending on knowing that the zoomed rollover
// // is drawn with clip optimization off.
// dc.g.setColor(fillColor);
// dc.g.fill(getZeroShape());
// }
else {
// This should be an even safer test... tho why are white text slide
// titles on black slides *sometimes* dissapearing when auto-zoomed?
// Could this have anything to do with our hack to auto-patch-up
// text colors on matching backgrounds?
if (fillColor.equals(dc.getBackgroundFill())) {
dc.g.setColor(fillColor);
dc.g.fill(getZeroShape());
} else {
dc.fillArea(getZeroShape(), fillColor);
}
}
if (master != null) {
// As the master slide isn't in the model, sit's children can't succesfully know
// their bounds anyway, so we can't clip-optimize further when we draw it.
// (It would be of little help anyway)
final DrawContext masterDC = dc.push();
masterDC.setClipOptimized(false);
// We only draw the master's children, as we've already
// done our background fill:
master.drawChildren(masterDC);
dc.pop();
}
if (DEBUG.BOXES) {
dc.g.setColor(Color.red);
dc.setAbsoluteStroke(1);
dc.g.draw(getZeroShape());
} else if (onMapSlideIcon && !drewBorder /*&& !dc.isAnimating()*/ && !dc.isPrintQuality()) { // need: isPrinting
// force a basic slide-icon border in case fill has no contrast w/background
dc.g.setColor(Color.darkGray);
dc.g.setStroke(STROKE_FIVE);
dc.g.draw(getZeroShape());
}
if (dc.focal != this)
dc.g.clip(getZeroShape());
// Now draw the slide contents:
drawChildren(dc);
}
@Override
public boolean canDuplicate() {
return DEBUG.META; // testing only
}
@Override
public LWSlide duplicate(CopyContext cc)
{
if (!DEBUG.Enabled)
return null;
LWSlide newSlide = (LWSlide) super.duplicate(cc);
if (newSlide.mEntry == null && newSlide.isTransparent() && getMasterSlide() != null) {
// a dupe of an on-pathway slide will have no fill -- if it ended up
// w/out an entry (the default), grab it's last fill color.
newSlide.setFillColor(getMasterSlide().getFillColor());
}
return newSlide;
}
@Override
public boolean handleDoubleClick(MapMouseEvent e)
{
// todo: a VueAction or LWCAction fire call that takes an InputEvent and an LWComponent so we
// can track the source of these events
if (e == null) { // special case message from MapViewer
// can't use SHIFT, as 2nd click de-selects, option/command also seem problematic on Mac
// TODO: this action depends on this slide having been selected by
// the first click, and the pathway entrie being made the active entry
// before the action fires -- we should be passing in the LWSlide.
// Too hairy for now: leaves the hand tool selected;
//VUE.getSelection().setTo(this);
if (DEBUG.Enabled)
Actions.LaunchPresentation.fire("MapViewerHandToolDoubleClick");
//Actions.LaunchPresentation.fire(e);
} else {
Actions.EditSlide.act(this);
}
return true;
}
@Override
protected LWComponent pickChild(PickContext pc, LWComponent c) {
//if (DEBUG.PICK) out("PICKING CHILD: " + c);
if (pc.root == this || (!SwapFocalOnSlideZoom && pc.dc.zoom > 4))
return c;
else
return this;
}
// /** @return this slide */
// @Override
// protected LWComponent defaultPick(PickContext pc) {
// //if (DEBUG.PRESENT) out("DEFAULT PICK: THIS");
// return this;
// // LWComponent dp = (pc.dropping == null ? null : this);
// // out("DEFAULT PICK: " + dp);
// // return dp;
// }
@Override
protected LWComponent defaultDropTarget(PickContext pc) {
LWComponent c = super.defaultDropTarget(pc);
if (DEBUG.PRESENT) out("DEFAULT DROP TARGET: " + c);
return c;
}
}