/* * 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; } }