/* * 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 static tufts.vue.VueConstants.*; import static tufts.vue.LWPathway.Entry; import static tufts.vue.MapViewer.*; import tufts.Util; import tufts.macosx.MacOSX; import tufts.vue.gui.GUI; import tufts.vue.gui.TextRow; import java.util.*; import java.awt.Color; import java.awt.Font; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.AlphaComposite; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RectangularShape; import java.awt.geom.RoundRectangle2D; import java.awt.event.*; import javax.swing.*; import edu.tufts.vue.preferences.implementations.BooleanPreference; /** * Tool for presentation mode. * * Handles presentation mode key commands and screen decorations. * * @version $Revision: 1.418 $ / $Date: 2007/05/06 20:14:17 $ / $Author: sfraize $ * @author Scott Fraize * */ public class PresentationTool extends VueTool implements LWComponent.Listener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(PresentationTool.class); private static final boolean AnimateAcrossMap = false; private static final boolean AnimateInOutMap = false; private static final boolean AnimateTransitions = AnimateAcrossMap || AnimateInOutMap; private static final String FORWARD = "FORWARD"; private static final String BACKWARD = "BACKWARD"; private static final boolean RECORD_BACKUP = true; private static final boolean BACKING_UP = false; static Object obj[] = {Actions.ToggleSlideIcons.getKeyStrokeDescription()}; private static final BooleanPreference ChooseNodesOverSlides = BooleanPreference.create (edu.tufts.vue.preferences.PreferenceConstants.PRESENTATION_CATEGORY, "slideNodeView", VueResources.getString("preference.slidenodeview.title"), VueResources.getMessageString("preference.slidenodeview.descriptiontwo", obj), VueResources.getString("preference.slidenodeview.descriptionone"), Boolean.FALSE, true); public static final BooleanPreference AutoZoomPreference = BooleanPreference.create (edu.tufts.vue.preferences.PreferenceConstants.PRESENTATION_CATEGORY, "autoZoom", VueResources.getString("preference.autozoom.title"), VueResources.getString("preference.autozoom.descriptionone"), VueResources.getString("preference.autozoom.descriptiontwo"), Boolean.FALSE, true); private final ImageButton ZoomButton = new ImageButton("zoomOut", VueResources.getImageIcon("pathwayTool.zoomOutImageIcon")){ void doAction() { out("ZOOM BUTTON ACTION"); boolean handled=false; if (!Actions.Rename.isUserEnabled() && // messy: encoding that we know Rename uses ENTER here... !(mFocal instanceof LWMap) && !(VUE.getActiveViewer() instanceof tufts.vue.ui.SlideViewer)) { // total SlideViewer hack... ZoomButton.setIcon(VueResources.getImageIcon("pathwayTool.zoomBackImageIcon")); handled = VUE.getActiveViewer().popFocal(false, true); } if (!handled) { ZoomButton.setIcon(VueResources.getImageIcon("pathwayTool.zoomOutImageIcon")); handleEnterKey(); } } void trackVisible(MouseEvent e) { // set us visible if the given mouse even is between us and the lower right hand corner of the screen if (!Actions.Rename.isUserEnabled() && // messy: encoding that we know Rename uses ENTER here... !(mFocal instanceof LWMap) && !(VUE.getActiveViewer() instanceof tufts.vue.ui.SlideViewer)) { ZoomButton.setIcon(VueResources.getImageIcon("pathwayTool.zoomOutImageIcon")); } else ZoomButton.setIcon(VueResources.getImageIcon("pathwayTool.zoomBackImageIcon")); //System.out.println("e.x : " + e.getX() + " e.y : " + e.getY() + " x : " + (x+width) + " y : " + y); setVisible(e.getX() > x && e.getY() > y); } }; private final ImageButton ExitButton = new ImageButton("exit", VueResources.getImageIcon("pathwayTool.exitImageIcon")) { void doAction() { if (VUE.inFullScreen()) VUE.toggleFullScreen(false, true); VUE.setActive(VueTool.class, this, VueTool.getInstance(SelectionTool.class)); } void trackVisible(MouseEvent e) { // set us visible if the given mouse even is between us and the lower right hand corner of the screen setVisible(e.getX() < (x+width) && e.getY() > y); } }; public static final String ResumeActionName = "Resume Presentation"; public static void ResumePresentation() { if (!VUE.inNativeFullScreen() && !VueUtil.isUnixPlatform()) VUE.toggleFullScreen(true); else if (!VUE.inNativeFullScreen() && VueUtil.isUnixPlatform()) { final PresentationTool presTool = PresentationTool.getTool(); //tufts.vue.Actions.LaunchPresentation.act(); presTool.handleFullScreen(true, true); //VUE.toggleFullScreen(true); GUI.invokeAfterAWT(new Runnable() { public void run() { VUE.toggleFullScreen(true); }}); GUI.invokeAfterAWT(new Runnable() { public void run() { //VueToolbarController.getController().setSelectedTool(presTool); VUE.setActive(VueTool.class, this, presTool); }}); GUI.invokeAfterAWT(new Runnable() { public void run() { presTool.startPresentation(); }}); //VUE.setActive(VueTool.class, , VueTool.getInstance(SelectionTool.class)); } } private final ImageButton ResumeButton = new ImageButton("resume", VueResources.getImageIcon("pathwayTool.resumeImageIcon")) { void doAction() { if (DEBUG.PRESENT) out("resume pressed"); ResumePresentation(); } @Override void setVisible(boolean visible) { super.setVisible(visible); if (visible) Actions.ToggleFullScreen.setActionName(ResumeActionName); else Actions.ToggleFullScreen.revertActionName(); } }; private JButton mStartButton; private final JCheckBox mShowContext = new JCheckBox("Show Context"); private final JCheckBox mToBlack = new JCheckBox("Black"); private final JCheckBox mZoomLock = new JCheckBox("Lock 100%"); private final Page NO_PAGE = new Page((LWComponent)null); private volatile Page mCurrentPage = NO_PAGE; private volatile Page mLastPage = NO_PAGE; private volatile Page mLastSlidePage; private static volatile LWPathway mPathway; // current pathway (last pathway we were on) private volatile LWPathway mStartPathway; // pathway when started presentation private volatile Page mLastPathwayPage; private volatile Page mLastStartPathwayPage; private volatile LWComponent mFocal; // sync'd with MapViewer focal private volatile LWComponent mNextPage; // is this really "startPage"? private static volatile boolean mFadeEffect = true, mShowOverview = DEBUG.NAV, mShowNavNodes, mForceShowNavNodes, mDidAutoShowNavNodes, mScreenBlanked; private class ImageButton { ImageIcon icon; final int width, height; final String name; int x, y; boolean visible; ImageButton(String name, ImageIcon icon) { this.icon = icon; this.name = name; width = icon.getIconWidth(); height = icon.getIconHeight(); } boolean isVisible() { return visible; } /** * set us visible if the given mouse (motion) event is somewhere on the screen * such that we want to appear. Default always makes the button visible. */ void trackVisible(MouseEvent e) { setVisible(true); } void setIcon(ImageIcon ic) { icon = ic; } void setVisible(boolean t) { if (visible != t) { visible = t; PresentationTool.this.repaint(); } } void setLocation(int x, int y) { if (DEBUG.PRESENT) out(this + " setLocation " + x + "," + y); this.x = x; this.y = y; } void draw(DrawContext dc) { dc.g.drawImage(icon.getImage(), x, y, null); } boolean contains(MouseEvent e) { boolean hit = contains(e.getX(), e.getY()); //if (DEBUG.PRESENT) out(this + " contains=" + hit + "; " + e); return hit; } private boolean contains(int x, int y) { if (!isVisible()) return false; return x >= this.x && y >= this.y && x <= this.x + width && y <= this.y + height; } void doAction() {} public String toString() { return String.format("ImageButton[%6s; %3d,%3d %s]", name, x, y, isVisible() ? "VISIBLE" : "HIDDEN"); } } /** a presentation moment (data for producing a single presentation screen) */ private static class Page { final LWPathway.Entry entry; final LWComponent node; Page(LWPathway.Entry e) { entry = e; node = null; } Page(LWComponent c) { if (c instanceof LWSlide) { if (((LWSlide)c).getPathwayEntry() != null) { entry = ((LWSlide)c).getPathwayEntry(); node = null; } else { if (DEBUG.Enabled) Util.printStackTrace("creating page for slide w/out entry: " + c); entry = null; node = c; } } else { LWPathway.Entry slideInsteadOfNode = null; if (// mPathway != null // if we have a current pathway !LWPathway.isShowingSlideIcons() // and slide icons are turned off && ChooseNodesOverSlides.isFalse() && c instanceof LWNode // and this is a node && c.hasEntries()) // and it has slide views available (is on any pathway) { LWPathway.Entry fallbackEntry = null; int fallbackCount = 0; for (LWPathway.Entry e : c.getEntries()) { if (e.pathway == mPathway) { // we've found a slide on the current pathway (which should always be visible) slideInsteadOfNode = e; break; } if (e.pathway.isDrawn()) { fallbackEntry = e; fallbackCount++; } } if (slideInsteadOfNode == null && fallbackCount == 1) slideInsteadOfNode = fallbackEntry; } if (slideInsteadOfNode != null) { entry = slideInsteadOfNode; node = null; } else { entry = null; node = c; } } } /** @return the object we're actually going to draw on screen for this presentation moment */ public LWComponent getPresentationFocal() { return entry == null ? node : entry.getFocal(); } /** @return the map node that is the source of this presentation item. * May return null if the this is a pathway entry that was a combo slide. */ public LWComponent getOriginalMapNode() { if (node != null) return node; else if (entry != null) return entry.node; return null; } public boolean isMapView() { if (entry == null) return node != null && node.getAncestorOfType(LWSlide.class) == null; else return entry.isMapView(); } // /** @return true if this is a map-view node that's on a pathway */ // public boolean isMapViewNode() { // if (entry == null) // return true; // changed 2007-10-22 // //return false; // else // return entry.isMapView(); // } public boolean isOnMapNode() { return entry == null; } public boolean isMap() { return node instanceof LWMap; } public boolean onPathway() { return entry != null; } public LWPathway pathway() { return entry == null ? null : entry.pathway; } /** @return true of this page is directly on the given pathway */ public boolean onPathway(LWPathway p) { if (p == null) return false; else if (node != null) return node.inPathway(p); else if (entry != null) return entry.pathway == p; else return false; } /** @return true of this page is inside something on the current * pathway (e.g., inside a slide on the pathway). Items ON the current pathway are excluded. */ public boolean inPathway(LWPathway p) { if (p == null) return false; else if (node != null && !node.inPathway(p)) { // If the node for this page is embedded in a slide, and that slide // is in the pathway, consider us in-pathway. We actually may NOT // want to do this, as it's a bit complicating... // TODO: handle map-nodes (groups) -- technically, we want // to check every ancestor for the path membership... final LWSlide slide = (LWSlide) node.getAncestorOfType(LWSlide.class); return slide != null && slide.inPathway(p); } else return false; } public boolean insideSlide() { return node != null && node.getParentOfType(LWSlide.class) != null; } public String getDisplayLabel() { if (node != null) return node.getDisplayLabel(); else if (entry != null) return entry.getLabel(); else { Log.warn("Page with no node or entry"); return "<NO-NODE-OR-ENTRY>"; } } public String toString() { //String s = "PAGE: "; String s = ""; if (node != null) s += node; else s += entry; return s; } @Override public boolean equals(Object o) { if (this == o) { return true; } else if (o instanceof Page) { final Page other = (Page) o; return entry == other.entry && node == other.node; //return (entry != null && entry == other.entry) || (node != null && node == other.node); // if (entry == other.entry && node == other.node) // return true; // else // return node == other.node && isMapView() == other.entry.isMapView(); } else if (o instanceof LWComponent) { final LWComponent c = (LWComponent) o; if (node == c || (entry != null && entry.node == c)) { return true; } else if (c instanceof LWSlide) { // SMF 2007-11-14 added new case for Page.equals -- test for side effects... LWSlide slide = (LWSlide) c; return entry != null && entry == slide.getPathwayEntry(); } else return false; } else return false; } } /** * Implements a stack/queue that works like standard web browser forward/back page * visit lists: You can always go back in a straight line, and you can keep going * forward in a straight line until you push something else on the stack (browse off * the current already visited list), in which case everything forward of where you * are in the list is throw away. */ private static class PageQueue<T> extends ArrayList<T> { int index; public boolean isEmpty() { // somehow, ArrayList is getting to a size == -1 condition, // and the default isEmpty checks size == 0, so it doesn't // think it's empty in that case! return size() <= 0; } public void rollBack() { if (index > 0) index--; else throw new IndexOutOfBoundsException("index=" + index); } public void rollForward() { if (index < lastIndex()) index++; else throw new IndexOutOfBoundsException("index=" + index + " lastIndex=" + lastIndex()); } private final int lastIndex() { return size() - 1; } public T first() { return size() <= 0 ? null : get(0); } public T last() { return size() <= 0 ? null : get(lastIndex()); } public T jumpFirst() { index = 0; return first(); } public T jumpLast() { index = lastIndex(); return last(); } public void push(T o) { // never allow null in the stack, or the same item repeated on the top of the stack if (o == null) return; if (o.equals(next())) { rollForward(); } else if (o.equals(current())) { ; // do nothing: never repeat items in the queue } else { if (!isEmpty() && index < lastIndex()) { // toss out forward stack information whenever we push anything: removeRange(index+1, size()); } add(o); index = size() - 1; } } public T current() { return isEmpty() ? null : get(index); } public boolean hasPrev() { return !isEmpty() && index > 0; } public boolean hasNext() { return index < lastIndex(); } /** * @return the next item back in the queue without changing the queue position. * Will return null if hasPrev() returns false. */ public T prev() { return hasPrev() ? get(index - 1) : null; } public T next() { return hasNext() ? get(index + 1) : null; } /** move the queue position back one, and return what's there */ public T popPrev() { if (!hasPrev()) throw new NullPointerException(this + " empty; can't pop"); return get(--index); } public T popNext() { if (!hasNext()) throw new NullPointerException(this + "index="+index + "; size="+size() + "; already at end"); return get(++index); } } private static PresentationTool singleton; private final PageQueue<Page> mVisited = new PageQueue(); private final List<NavNode> mNavNodes = new java.util.ArrayList(); private static final Font NavFont = new Font("SansSerif", Font.PLAIN, 16); private static final Font NavFontBold = new Font("SansSerif", Font.BOLD, 16); private static final Font NavFontItalic = new Font("SansSerif", Font.ITALIC, 16); private static final Font NavFontDebug = new Font("SansSerif", Font.PLAIN, 10); private static final Font NavFontBoldDebug = new Font("SansSerif", Font.BOLD, 10); private static final Font NavBoxFont = new Font("SansSerif", Font.PLAIN, 12); private static final Color NavFillColor = new Color(154,154,154); //private static final Color NavFillColor = new Color(64,64,64,96); private static final Color NavStrokeColor = new Color(96,93,93); //private static final Color NavTextColor = Color.darkGray; private static final Color NavTextColor = Color.black; private static final int SlideRoom = 30; private static final int BoxSize = 20; private static final int BoxGap = 5; private final class NavNode extends LWNode { static final int DefaultWidth = 200; static final int ActiveWidth = 300; static final int MaxWidth = ActiveWidth; final Page page; // destination page final String type; // for debug final boolean isOffEdge; final List<JumpBox> mJumpBoxes; /** * A sub-NavNode hit region for jumping to another view of the given node. * Generally this is a view of the node on another pathway, but also * includes support for the special case of the off-pathway, in-map * pure node. */ private final class JumpBox extends java.awt.geom.RoundRectangle2D.Float { final LWPathway.Entry entry; final TextRow row; final Color color; JumpBox(LWPathway.Entry e, float x, float y) { super(x, y, BoxSize, BoxSize, 5, 5); entry = e; if (entry != null) { if (DEBUG.Enabled) { String text = Integer.toString(entry.index() + 1); // index's are zero-based, so add 1 //if (entry.isLast()) text = "(" + text + ")"; row = TextRow.instance(text, NavBoxFont); } else row = null; color = Util.alphaMix(entry.pathway.getColor(), Color.white); } else { color = null; row = null; } } Page getPage() { return entry == null ? new Page(page.getOriginalMapNode()) : new Page(entry); } void draw(DrawContext dc) { final Color faint; if (color != null) { dc.g.setColor(color); dc.g.fill(this); faint = color.darker(); } else faint = null; if (DEBUG.Enabled && entry != null && entry.isLast()) { int left = (int) Math.round(super.x) + 4; int right = (int) Math.round(super.x + super.width) - 4; int bottom = (int) Math.round(super.y + super.height) - 4; dc.g.setStroke(STROKE_ONE); dc.g.setColor(faint); dc.g.drawLine(left, bottom, right, bottom); } if (entry == null) { dc.g.setStroke(page.entry == null ? STROKE_THREE : STROKE_ONE); } else if (page.entry != null && page.entry.pathway == entry.pathway) dc.g.setStroke(STROKE_THREE); else dc.g.setStroke(STROKE_ONE); dc.g.setColor(Color.darkGray); dc.g.draw(this); if (row != null) { dc.g.setFont(NavBoxFont); dc.g.setColor(faint); row.drawCenter(dc, this); } } public String toString() { return "Box[" + entry + "]"; } } private Rectangle2D.Float mJumpBoxMask; private List<JumpBox> createPathwayJumpBoxes(LWComponent node) { final List<LWPathway.Entry> entries = node.getEntries(); final List<JumpBox> boxes = new ArrayList(entries.size()); float rightEdge = getWidth() - (BoxGap + 5); if (isOffEdge) rightEdge += (NavLayout.InsetOffEdge - NavLayout.InsetIndent); final float y = (getHeight() - BoxSize) / 2f; float x = rightEdge - BoxSize; boxes.add(new JumpBox(null, x, y)); // add the node-only pathway box float leftEdge = x; x -= (BoxSize + BoxGap); for (ListIterator<Entry> i = entries.listIterator(entries.size()); i.hasPrevious();) { final Entry e = i.previous(); if (e.pathway.isDrawn()) { boxes.add(new JumpBox(e, x, y)); leftEdge = x; x -= BoxSize + BoxGap; } } if (mJumpBoxMask == null) mJumpBoxMask = new Rectangle2D.Float(); final Rectangle2D.Float mask = mJumpBoxMask; final int sidePad = 3; mask.x = leftEdge - (sidePad + 1); mask.y = y - 3; mask.width = (rightEdge - leftEdge) + sidePad * 2; mask.height = BoxSize + 7; return boxes; } static final String STANDOUT = "standout"; static final String NORMAL = "normal"; static final String ITALIC = "italic"; NavNode(Page destinationPage, boolean isOffEdge, String style, String type) { super(null); final boolean isStandout = (style == STANDOUT); if (destinationPage == null) throw new IllegalArgumentException(getClass() + "; destination page is null"); this.page = destinationPage; this.isOffEdge = isOffEdge; this.type = type; //if (DEBUG.WORK) out("new NavNode for page " + destinationPage + "; isLastPathway=" + isLastPathwayPage); final LWPathway pathway = null; String label; if (page.node != null && page.node instanceof LWMap) label = "Map View"; else label = page.getDisplayLabel(); // final LWPathway pathway; // if (destinationPage.entry != null) // pathway = destinationPage.entry.pathway; // else // pathway = null; // String label; // if (pathway != null) // label = pathway.getLabel(); // else // label = page.getDisplayLabel(); // if (pathway == null && destinationPage.equals(mVisited.prev())) // label = "<- " + label; if (label.length() > 20) label = label.substring(0,18) + "..."; //label = " " + label + " "; setLabel(label); if (style == STANDOUT) setFont(NavFontBold); else if (style == ITALIC) setFont(NavFontItalic); else // style == NORMAL setFont(NavFont); // if (DEBUG.PRESENT) // setFont(isStandout ? NavFontBoldDebug : NavFontDebug); // else // setFont(isStandout ? NavFontBold : NavFont); setTextColor(NavTextColor); if (false && pathway != null) { // Nav node for jumping to another pathway //final Color color = Util.alphaMix(pathway.getColor(), pathway.getMasterSlide().getFillColor()); final Color color = Util.alphaMix(pathway.getColor(), Color.gray); //final Color color = pathway.getColor(); mFillColor.setFixedAlpha(224); setFillColor(color); setTextColor(Color.black); setStrokeWidth(0); //setStrokeColor(color); //setStrokeWidth(3); } else { setStrokeColor(NavStrokeColor); setFillColor(NavFillColor); setStrokeWidth(1); } //setSyncSource(src); // TODO: these will never get GC'd, and will be updating for ever based on their source... //setShape(new RoundRectangle2D.Float(0,0, 10,10, 20,20)); // is default setAutoSized(false); int buttonWidth = isStandout ? ActiveWidth : DefaultWidth; setSize(buttonWidth + SlideRoom, getHeight() + 6); final LWComponent node = page.getOriginalMapNode(); // pull node from entry if node is null? if (node == null) { Log.debug("null node in " + this); //Util.printStackTrace("NULL NODE in " + this); mJumpBoxes = null; } else if (node.inVisiblePathway()) { mJumpBoxes = createPathwayJumpBoxes(node); } else { mJumpBoxes = null; } } Page hitPage(float x, float y) { if (containsLocalCoord(x, y)) { if (mJumpBoxes != null) { final float localX = x - getX(); final float localY = y - getY(); for (JumpBox box : mJumpBoxes) { if (box.contains(localX, localY)) { if (DEBUG.PRESENT || DEBUG.PICK) Log.debug("hit entry box " + box); return box.getPage(); } } } return this.page; } else return null; } @Override public void draw(DrawContext dc) { dc.setClipOptimized(false); // ensure all children draw even if not inside clip transformZero(dc.g); final LWComponent node = page.getOriginalMapNode(); //if (node.inPathway()) { if (mJumpBoxes != null) { drawZero(dc.push()); dc.pop(); drawPathwayJumpBoxes(dc); //drawPathwaySwatches(dc, node); } else { drawZero(dc); } if (DEBUG.Enabled) { dc.g.setFont(FONT_TINY); dc.g.setColor(Color.white); int y = -10; if (page.entry != null) { dc.g.drawString("Entry: " + page.entry, -100, y+=10); if (page.node != null) dc.g.drawString("NODE: " + page.node, -100, y+=10); } else { dc.g.drawString("Node: " + page.node, -100, y+=10); if (page.entry != null) dc.g.drawString("ENTRY: " + page.entry, -100, y+=10); } dc.g.drawString("Type: " + type, -100, y+=10); } // // Draw slide icon if there is one: // if (page.entry != null && page.entry.hasSlide()) { // drawSlideIcon(dc); // } } private void drawPathwayJumpBoxes(DrawContext dc) { if (DEBUG.PRESENT) dc.g.setColor(Color.lightGray); else dc.g.setColor(NavFillColor); // Clear out everything under the jump boxes, so the // text from long labels can never show through: dc.g.fill(mJumpBoxMask); for (JumpBox box : mJumpBoxes) box.draw(dc); } private void drawSlideIcon(DrawContext dc) { final LWSlide slide = page.entry.getSlide(); final double scale = (getHeight()-10) / slide.getHeight(); final double iconHeight = slide.getHeight() * scale; //final double iconWidth = slide.getWidth() * scale; dc.g.translate(getWidth() - (SlideRoom+12), (getHeight() - iconHeight)/2); dc.g.scale(scale, scale); dc.setBackgroundFill(null); // so if current slide is on screen, will still fill slide.drawZero(dc); //dc.g.setColor(Color.black); //dc.g.drawString("HELLO", 0, 0); } @Override public void setLocation(float x, float y) { super.takeLocation(x, y); } // These are all NOOP's: nobody needs our events: @Override protected void notify(String what, LWComponent contents) {} @Override protected void notify(String what, Object oldValue) {} @Override protected void notify(Key key, Object oldValue) {} @Override protected void notify(String what) {} @Override protected void notify(String what, List<LWComponent> componentList) {} @Override protected void notifyLWCListeners(LWCEvent e) {} // force label at left (non-centered) @Override protected final float relativeLabelX() { return -NavNodeX + 10; } } public PresentationTool() { super(); if (singleton != null) new Throwable("Warning: mulitple instances of " + this).printStackTrace(); singleton = this; VueToolUtils.setToolProperties(this,"viewTool"); VUE.addActiveListener(LWPathway.Entry.class, this); } public void activeChanged(ActiveEvent e, LWPathway.Entry entry) { if (e.isRefresh()) { // this fixes bug of auto-jumping into presentation (first slide) // from map-overview mode (popped focal to top) when toggling // slide icons on and off return; } if (entry == null) { // Last pathway was deleted: loadPathway(null); } else if (isActive()) { // only do this if this is the active tool, // as we use the globally active viewer // when we change the page! if (!entry.isPathway()) setEntry(entry, BACKING_UP); } } @Override public JPanel createToolPanel() { //JPanel p = super.createToolPanel(); JPanel p = new JPanel(); mStartButton = new JButton(VueResources.getString("presentationTool.startButton.label")); mToBlack.setSelected(false); mShowContext.setSelected(true); add(p, mStartButton); //add(p, mShowContext); //add(p, mToBlack); //p.add(mZoomLock, 0); return p; } /** return the singleton instance of this class */ public static PresentationTool getTool() { if (singleton == null) { // new Throwable("Warning: PresentationTool.getTool: class not initialized by VUE").printStackTrace(); //throw new IllegalStateException("NodeTool.getTool: class not initialized by VUE"); new PresentationTool(); } return singleton; } private void add(JPanel p, AbstractButton c) { c.setFocusable(false); c.setFont(VueConstants.FONT_SMALL); if (c instanceof JCheckBox) c.setContentAreaFilled(false); p.add(c, 0); c.addActionListener(this); } public void actionPerformed(ActionEvent ae) { if (ae.getSource() == mStartButton) { startPresentation(); VUE.toggleFullScreen(true); } else if (ae.getSource() instanceof JCheckBox) { repaint("checkBox"); } else super.actionPerformed(ae); } private void repaint() { repaint(""); } private void repaint(String msg) { if (DEBUG.WORK) out("repaint " + msg); VUE.getActiveViewer().repaint(); } private boolean onCurrentPathway() { final Page p = mCurrentPage; if (p == null || p == NO_PAGE || mPathway == null) return false; else return p.onPathway(mPathway); // This appears to be checking if we've descended into the contents of a pathway page... //|| (p.node != null && page.node.getParent() == mPathway); // page.node != entry.node !! } private boolean inCurrentPathway() { final Page page = mCurrentPage; if (page == null || page == NO_PAGE || mPathway == null) return false; else return page.inPathway(mPathway); } private void revisitPrior() { revisitPrior(false); } private void revisitPrior(boolean allTheWay) { if (DEBUG.PRESENT) out(String.format("revisitPrior%s", allTheWay ? " (AllTheWay)" : "")); if (allTheWay) setPage(mVisited.jumpFirst()); else if (mVisited.hasPrev()) setPage(mVisited.popPrev(), BACKING_UP); } private void revisitNext() { revisitNext(false); } private void revisitNext(boolean allTheWay) { if (allTheWay) setPage(mVisited.jumpLast()); else if (mVisited.hasNext()) setPage(mVisited.popNext(), BACKING_UP); } private void goBackward(boolean allTheWay) { if (onCurrentPathway()) { if (allTheWay) { // todo: skip to start of queue if been there instead // adding more to the end! setEntry(mPathway.getFirst()); } else { setEntry(nextPathwayEntry(BACKWARD, allTheWay), BACKING_UP); } } else { if (DEBUG.NAV) revisitPrior(allTheWay); } } private static final boolean SINGLE_STEP = false; private static final boolean GUESSING = true; private boolean goForward(boolean allTheWay) { return goForward(allTheWay, false); } /** @return true if there was a meaninful way forward that was taken (even if current page doesn't change: e.g., end of pathway) */ private boolean goForward(boolean allTheWay, boolean guessing) { boolean wentForward = true; if (mCurrentPage == NO_PAGE && mNextPage == null) { startPresentation(); } else if (onCurrentPathway()) { if (false && allTheWay) { // todo: skip to end of queue if been here instead // of blowing it away! // skipping far forward complicates the hell // out of maintaining a sane seeming backlist... setEntry(mPathway.getLast()); } else { setEntry(nextPathwayEntry(FORWARD, allTheWay)); } } else { if (guessing) { if (false && inCurrentPathway()) { // This too hairy for now: we're attempting to allow "space" // to auto-pop back up to a slide whose contents we've delved into, // tho we really need to be checking the back-list to make sure // the prior member is also either inside the current pathway, // or is the item on the pathway itself. revisitPrior(); } else if (!mForceShowNavNodes) { // if we hit space and there's nowhere to go, at leasts how them their options: mForceShowNavNodes = mShowNavNodes = true; mDidAutoShowNavNodes = true; repaint("guessForward"); } wentForward = false; } else { if (DEBUG.NAV) revisitNext(allTheWay); else wentForward = false; } } return wentForward; } @Override public PickContext initPick(PickContext pc, float x, float y) { // allow picking of node icons, group contents, etc as long as not a at top level map if (mFocal instanceof LWMap == false) pc.pickDepth = 1; return pc; } private boolean ShowOverviewKeyIsDown = false; @Override public boolean handleKeyPressed(java.awt.event.KeyEvent e) { if (DEBUG.PRESENT) out(" handleKeyPressed " + e); final int keyCode = e.getKeyCode(); final char keyChar = e.getKeyChar(); boolean handled = true; final boolean amplified = e.isShiftDown(); switch (keyCode) { case KeyEvent.VK_TAB: if (!mShowOverview) { ShowOverviewKeyIsDown = true; mShowOverview = true; repaint("tmpOverview"); } break; case KeyEvent.VK_ENTER: // [2007-10-22:this comment still current? ] // MapViewer popFocal must have failed -- focal should be the map: // todo: all our VueTool handled calls that come from the MapViewer // should take a viewer as an argument... handleEnterKey(); break; case KeyEvent.VK_R: if (ResumeButton.isVisible()) ResumeButton.doAction(); else setPage(mLastStartPathwayPage); break; case KeyEvent.VK_SPACE: if (e.isShiftDown()) goBackward(false); else doDefaultForwardAction(); break; case KeyEvent.VK_DOWN: if (DEBUG.NAV) { if (mVisited.hasNext()) revisitNext(amplified); else goForward(amplified); break; } // else fallthru to RIGHT case KeyEvent.VK_RIGHT: goForward(amplified); break; case KeyEvent.VK_UP: if (DEBUG.NAV) { revisitPrior(amplified); break; } // else fallthru to LEFT case KeyEvent.VK_LEFT: goBackward(amplified); break; //case KeyEvent.VK_BACK_QUOTE: case KeyEvent.VK_N: // toggle showing the non-linear nav options: //mForceShowNavNodes = mShowNavNodes = !mForceShowNavNodes; mForceShowNavNodes = mShowNavNodes = !mShowNavNodes; repaint("toggleNav="+mShowNavNodes); break; // case KeyEvent.VK_T: // if (Actions.ToggleSlideIcons.fireIfMatching(e)) // break; default: handled = false; } if (handled) return true; handled = true; switch (keyChar) { // case '1': // if (mPathway != null) { // mPathwayIndex = 0; // mNextPage = mPathway.getNodeEntry(0); // forwardPage(); // } // repaint(); // break; case 'F': mFadeEffect = !mFadeEffect; break; // case 'C': // case 'N': // mShowContext.doClick(); // break; // case 'B': // mToBlack.doClick(); // break; case 'M': case 'm': mShowOverview = !mShowOverview; repaint("showOverview"); break; case 'T': case 't': Actions.ToggleSlideIcons.fire(this, e); break; case 'p': PathwayPanel.TogglePathwayExclusiveFilter(); break; case '+': case '=': // allow "non-shift-plus" if (isOverviewVisible() && OverviewMapSizeIndex < OverviewMapScales.length-1) { OverviewMapSizeIndex++; repaint("overviewBigger"); } else handled = false; break; case '-': case '_': // allow "shift-minus" also if (isOverviewVisible() && OverviewMapSizeIndex > 0) { OverviewMapSizeIndex--; repaint("overviewSmaller"); } else handled = false; break; case '~': DEBUG.NAV = !DEBUG.NAV; repaint(); break; default: handled = false; } //if (handled && DEBUG.KEYS) out("HANDLED " + keyChar); return handled; } private void handleEnterKey() { if (mFocal instanceof LWMap && mLastPage != null) { setPage(mLastPage); } else if (mVisited.hasPrev()) { setPage(mVisited.prev()); } else if (mLastPathwayPage != null) { setPage(mLastPathwayPage); } else { // non-pathway navigation: just zoom back out to entire map VUE.getActiveViewer().fitToFocal(); // TODO: viewer should be passed in.. // TODO: want to animate zoom! // Really need that Page object, probably at the MapViewer level, // that knows if we've EVER been zoomed and/or panned off the main focal, // so that we could "reload" the focal by zoom-fitting to it even // if it's the same focal } } @Override public boolean handleKeyReleased(java.awt.event.KeyEvent e) { if (DEBUG.PRESENT) out("handleKeyReleased " + e); if (e.getKeyCode() == KeyEvent.VK_TAB) { // dismiss even if it had been displayed manually //if (ShowOverviewKeyIsDown && e.getKeyCode() == KeyEvent.VK_TAB) { // dismiss only if displayed via holding TAB key down ShowOverviewKeyIsDown = false; if (mShowOverview) { mShowOverview = false; repaint("removeTmpOverview"); } } return false; } //private boolean isPresenting() { return !mShowContext.isSelected(); } private static float[] OverviewMapScales = {8, 6, 4, 3, 2.5f, 2, 1.5f, 1.25f, 1}; private static int OverviewMapSizeIndex = 5; private float mNavMapX, mNavMapY; // location of the overview navigator map private DrawContext mNavMapDC; private final Color OverviewMapFill = Color.gray; // black or white depending on brightess of the fill? private final Color OverviewMapFillAlpha = new Color(128,128,128,224); /** Draw a ghosted panner */ private void drawOverviewMap(DrawContext dc) { dc.setFrameDrawing(); final float overviewMapFraction = OverviewMapScales[OverviewMapSizeIndex]; final Rectangle overview = new Rectangle(0,0, (int) (dc.frame.width / overviewMapFraction), (int) (dc.frame.height / overviewMapFraction)); mNavMapX = dc.frame.width - overview.width; mNavMapY = dc.frame.height - overview.height; dc.g.translate(mNavMapX, mNavMapY); if (overviewMapFraction == 1 || DEBUG.Enabled) dc.g.setColor(OverviewMapFillAlpha); else dc.g.setColor(OverviewMapFill); dc.g.fill(overview); // clip in case borders that might extend outside the overview if the fit is tight to the raw map shapes dc.g.clipRect(0,0, overview.width, overview.height); //dc.setMasterClip(overview); final MapViewer viewer = VUE.getActiveViewer(); // TODO: pull from some kind of context final LWComponent onMapNode = mCurrentPage.getOriginalMapNode(); final LWPathway.Entry entry = mCurrentPage.entry; final LWPathway pathway; // if non-null, we're inside a slide (at any depth), and this is it final LWSlide inSlide; // the "standout" node is the node on the map we're going to show highlighted LWComponent standout = mCurrentPage.getPresentationFocal(); if (standout == null || !standout.isVisible()) { // if standout is slide icon and slide icons are turned off, will report not visible standout = onMapNode; } if (standout != null) { // if we're nav-clicking within a slide, the original map node is totally // uninteresting: it's just the node (e.g., an image) on the slide -- if // slides we're really on the map tho, we could in fact zoom to it normally. //final LWSlide slide = (LWSlide) standout.getAncestorOfType(LWSlide.class); // TODO: Test using getParent v.s. getAncestor... inSlide = (LWSlide) standout.getParentOfType(LWSlide.class); if (inSlide != null) standout = inSlide; //standout = slide.getSourceNode(); } else inSlide = null; if (inSlide != null && inSlide.getEntry() != null) pathway = inSlide.getEntry().pathway; else if (entry != null) pathway = entry.pathway; else pathway = null; //----------------------------------------------------------------------------- // done sorting out and establishing context state: now start drawing //----------------------------------------------------------------------------- dc.skipDraw = standout; // will be ignored! We only pass GC into paintViewerIntoRectangle //final LWMap map = viewer.getMap(); //dc.setDrawPathways(true); //map.drawFit(dc.push(), overview, 10); dc.pop(); // if (DEBUG.WORK) out("painting viewer for overview: " + viewer // + ";\n\twith standount=" + standout // + ";\n\tslideIcons=" + LWPathway.isShowingSlideIcons()); final Graphics2D pannerGC = (Graphics2D) dc.g.create(); mNavMapDC = MapPanner.paintViewerIntoRectangle(null, pannerGC, viewer, overview, false); // draw reticle //---------------------------------------------------------------------------------------- // would be cleaner to do this way now that we have to recapitulate the panner reticle // functionality here to deal with specialized standout / reticle code and hacks // for those nefarious slide icons. // Also, we could then set dc.isPresenting in the dc so all slide icons paint their pathway borders. //viewer.getMap().drawFit(dc, overview, 0); //---------------------------------------------------------------------------------------- if (mNavMapDC == null) { Log.error("null nav map GC for viewer " + viewer); return; } if (standout == null) { if (DEBUG.Enabled) Log.debug("no standout item via page " + mCurrentPage); mNavMapDC.dispose(); return; } //----------------------------------------------------------------------------- // First, fade out everything already drawn on the map, so we can then // redraw a standout item later that appears brighter than everything else //----------------------------------------------------------------------------- dc.g.setColor(Color.black); dc.setAlpha(0.5); dc.g.fill(overview); //----------------------------------------------------------------------------- // Second, redraw the pathway the current page is part of (if any), // so it can stand out as well. //----------------------------------------------------------------------------- if (pathway != null) { // redraw the current pathway so it's highlighted pathway.drawPathwayWithDots(mNavMapDC); } // highlight connectd nav nodes if non-linear nav overlay is showing: // if (false && mShowNavNodes) { // for (NavNode nav : mNavNodes) { // if (nav.page.node != null && nav.page.node != standout) { // nav.page.node.draw(dc.push()); dc.pop(); // } // } // } //----------------------------------------------------------------------------- // now redraw the standout item, using the original overview map GC, which // is still at full brighness (alpha=1) //----------------------------------------------------------------------------- //if (DEBUG.WORK) out("overview drawing standout: " + standout); DrawContext standoutDC = new DrawContext(mNavMapDC, standout); if (standout instanceof LWSlide) { // only slides attend to focused for this, and currently // it will conflict with LWImage using it for setIndicated standoutDC.focused = standout; } standout.draw(standoutDC); standoutDC.dispose(); // Attempting to force pathway borders to redraw on slide icons: // focalDC.setDrawPathways(true); // focalDC.setClipOptimized(false); // standout.transformZero(focalDC.g); // standout.drawZeroDecorated(focalDC, false); //----------------------------------------------------------------------------- // now draw a special reticle depending on what's selected // is still at full brighness (alpha=1) //----------------------------------------------------------------------------- //if (!(standout instanceof LWSlide) && mCurrentPage.isMapView() && bounds != null) { if (standout instanceof LWMap == false) { // draw the red reticle final Rectangle2D.Float reticle; if (onMapNode instanceof LWLink) { reticle = onMapNode.getFocalBounds(); Util.grow(reticle, 2); } else { if (entry != null) { reticle = onMapNode.getPaintBounds(); Util.grow(reticle, 2); if (onMapNode.hasEntries() && entry.pathway.isShowingSlides()) { // include room for the slide icon pathway border stroke: reticle.width += 5; reticle.height += 5; } } else { reticle = onMapNode.getMapBounds(); Util.grow(reticle, 3); } //reticle = standout.getPaintBounds(); //reticle = mFocal.getPaintBounds(); } // if (viewer.getFocal() instanceof LWSlide) { // if (standout != null) // //reticle = standout.getPaintBounds(); // reticle = standout.getBounds(); // could grab node icon bounds if they're drawing... // } else { // reticle = viewer.getVisibleMapBounds(); // } // redraw the reticle at full brightness: mNavMapDC.setAntiAlias(true); //if (DEBUG.WORK) out("overview showing map bounds for: " + mCurrentPage + " in " + viewer + " bounds " + reticle); // mNavMapDC.g.setColor(Color.white); // mNavMapDC.setAlpha(0.2); // mNavMapDC.g.fill(reticle); mNavMapDC.setAlpha(1); mNavMapDC.g.setColor(Color.red); //mNavMapDC.g.setStroke(VueConstants.STROKE_TWO); mNavMapDC.setAbsoluteStroke(2); mNavMapDC.g.draw(reticle); } //if (DEBUG.WORK) out("overview drawing standout: " + standout); //dc.setAlpha(1); //standout.draw(mNavMapDC.push()); mNavMapDC.pop(); mNavMapDC.dispose(); //pannerGC.dispose(); // owned by mNavMapDC } /** @return true to disable rollovers on the map */ @Override public boolean handleMouseMoved(MapMouseEvent e) { // if (viewer.getFocal() instanceof LWMap) { // // never any nav nodes if a map is the focus // mShowNavNodes = false; // return true; // } ExitButton.trackVisible(e); ZoomButton.trackVisible(e); if (mForceShowNavNodes) { // no state to change return true; } final MapViewer viewer = e.getViewer(); final int width = viewer.getWidth(); final int MouseRightActivationPixel = width - 40; final int MouseRightClearAfterActivationPixel = width - NavNode.MaxWidth; if (DEBUG.MOUSE && DEBUG.META) { Log.debug(String.format("CURX %d; Max %d; On pixel: %d, off pixel %d", e.getX(), width, MouseRightActivationPixel, MouseRightClearAfterActivationPixel )); } if (DEBUG.Enabled && isOverviewVisible()) debugTrackNavMapMouseOver(e); boolean oldShowNav = mShowNavNodes; if (e.getX() > MouseRightActivationPixel) { if (DEBUG.MOUSE) Log.debug("nav nodes on at mouse " + e.getX()); mShowNavNodes = true; mForceShowNavNodes = false; if (DEBUG.MOUSE && oldShowNav) Log.debug("nav nodes should already be visible"); } else { if (mShowNavNodes && !mForceShowNavNodes) { if (e.getX() < MouseRightClearAfterActivationPixel) { mShowNavNodes = false; if (DEBUG.MOUSE) Log.debug("nav nodes off " + e.getX()); } } } if (oldShowNav != mShowNavNodes) repaint("mouseMove nav display change"); return false; } // /** @return true to disable rollovers on the map */ // @Override // public boolean handleMouseMoved(MapMouseEvent e) // { // boolean handled = false; // if (DEBUG.PICK && mShowOverview) // handled = debugTrackNavMapMouseOver(e); // boolean oldShowNav = mShowNavNodes; // //int maxHeight = e.getViewer().getVisibleBounds().height; // //if (e.getY() > maxHeight - 40) { // if (e.getX() < 40) { // //if (DEBUG.PRESENT) out("nav nodes on " + e.getY() + " max=" + maxHeight); // mShowNavNodes = true; // mForceShowNavNodes = false; // } else { // if (mShowNavNodes && !mForceShowNavNodes) { // if (e.getX() > 200) // mShowNavNodes = false; // } // //if (DEBUG.PRESENT) out("nav nodes off " + e.getY() + " max=" + maxHeight); // } // if (oldShowNav != mShowNavNodes) // repaint("mouseMove nav display change"); // //e.getViewer().repaint(); // return true; // } private final LWComponent HIT_OVERVIEW = new LWNode("<overmap-map-hit>"); private LWComponent getOverviewMapHit(MapMouseEvent e) { if (e.getX() < mNavMapX || e.getY() < mNavMapY) return null; //out("over map at " + e.getPoint()); Point2D.Float mapPoint = new Point2D.Float(e.getX() - mNavMapX, e.getY() - mNavMapY); out(mNavMapDC.toString()); //out("over nav map rect at " + mapPoint); mapPoint.x -= mNavMapDC.offsetX; mapPoint.y -= mNavMapDC.offsetY; mapPoint.x /= mNavMapDC.zoom; mapPoint.y /= mNavMapDC.zoom; //out("over nav map at " + mapPoint); final MapViewer viewer = e.getViewer(); final PickContext pc = new PickContext(viewer.getLastDC(), mapPoint.x, mapPoint.y); pc.root = viewer.getMap(); final LWComponent hit = LWTraversal.PointPick.pick(pc); out("hit=" + hit); return hit == null ? HIT_OVERVIEW : hit; } private void debugTrackNavMapMouseOver(MapMouseEvent e) { LWComponent hit = getOverviewMapHit(e); // //out(mNavMapDC.g.getClipBounds().toString()); // //if (mNavMapDC.g.getClipBounds().contains(e.getPoint())) { // if (e.getX() > mNavMapX && e.getY() > mNavMapY) { // //out("over map at " + e.getPoint()); // Point2D.Float mapPoint = new Point2D.Float(e.getX() - mNavMapX, // e.getY() - mNavMapY); // out(mNavMapDC.toString()); // out("over nav map rect at " + mapPoint); // mapPoint.x -= mNavMapDC.offsetX; // mapPoint.y -= mNavMapDC.offsetY; // mapPoint.x /= mNavMapDC.zoom; // mapPoint.y /= mNavMapDC.zoom; // out("over nav map at " + mapPoint); // final MapViewer viewer = e.getViewer(); // final PickContext pc = new PickContext(viewer.getLastDC(), mapPoint.x, mapPoint.y); // pc.root = viewer.getMap(); // final LWComponent hit = LWTraversal.PointPick.pick(pc); // //out("hit=" + hit); // if (hit != null) // e.getViewer().setIndicated(hit); // else // e.getViewer().clearIndicated(); // return true; // } else // return false; } private void focusUp(MapMouseEvent e) { focusUp(e, false); } private static final boolean TO_MAP = true; // todo: really, a total refocus, that handles the transition, // and knowing if a new focal needs loading private void focusUp(MapMouseEvent e, boolean forceMap) { if (DEBUG.PRESENT) out("focusUp, forceMap=" + forceMap); final boolean toMap = forceMap || e.isShiftDown(); //final boolean toLinks = e.isAltDown(); final MapViewer viewer = e.getViewer(); final LWComponent focal = viewer.getFocal(); //if (toMap) viewer.popFocal(toMap); // if (toLinks && focal.hasLinks()) { // GUI.invokeAfterAWT(new Runnable() { public void run() { // ZoomTool.setZoomFitRegion(viewer, // focal.getCenteredFanBounds(), // 30, // true); // }}); // } } /** toggle back and forth between either map and slide, or slide and descendent content */ private void toggleFocal(MapMouseEvent e) { if (DEBUG.PRESENT) out("toggleFocal"); if (mCurrentPage.isMap()) setPage(mLastPage); else focusUp(e, TO_MAP); } public boolean handleMouseClicked(MapMouseEvent e) { // ignore all clicks return true; } @Override public boolean handleMousePressed(MapMouseEvent e) { if (ExitButton.contains(e)) { ExitButton.doAction(); return true; } if (ZoomButton.contains(e)){ ZoomButton.doAction(); return true; } if (ResumeButton.contains(e)) { ResumeButton.doAction(); return true; } if (e.onFocus) { if (ResumeButton.isVisible() && e.isShiftDown()) { // shift-click anywhere on slide if we just gained focus is a shortcut // to resume the presentation. Note that onFocus will only be true if // VUE did NOT have OS application focus before the click -- e.g., on // the mac, Apple-TAB switching to VUE will give the focus to VUE, and // the next click will not be marked with onFocus in VUE. ResumeButton.doAction(); } // never advance/change focal on focus return true; } if (GUI.isRightClick(e)) { toggleFocal(e); return true; } LWComponent hit = null; if (isOverviewVisible()) { hit = getOverviewMapHit(e); if (hit != null && hit != HIT_OVERVIEW) { setPage(hit); if (e.isShiftDown()) mShowOverview = false; return true; } } if (hit == null && checkForNavNodeClick(e)) return true; //if (e.isShiftDown()) // return false; if (hit == null) hit = e.getPicked(); if (DEBUG.PRESENT) out("[" + e.paramString() + "] hit=" + hit); if (hit == HIT_OVERVIEW) // only needed in earlier design: should never hit this now return true; // if (hit instanceof LWLink && !(hit.getParent() instanceof LWMap)) { // // do nothing for links inside slide or group for now (display is jumpy/messy for groups) // return true; // } // if (hit == null) { // // if we hit nothing / or the focal, attempt to go forward down the pathway. // // If that's possible, let it happen -- if not, focus up. // if (!goForward(SINGLE_STEP)) // focusUp(e); // return true; // } if (hit != null) { // if (hit.getTypeToken() == LWNode.TYPE_TEXT || hit.isLikelyTextNode()) { // if (hit.getTypeToken() != LWNode.TYPE_TEXT) // Log.warn("likely text node has non-text type token: " + hit + "; token=" + hit.getTypeToken()); // if (hit.hasResource()) { // hit.getResource().displayContent(); // return true; // } // } if (hit.isExternalResourceLinkForPresentations()) { hit.getResource().displayContent(); return true; } else if (hit instanceof LWNode) { // only allow node for the moment // added to handle resource icon clicks (will call displayContent) if (hit.handleSingleClick(e)) return true; } // if no resource, do not just zoom to single text item //return true; } if (hit == null || mCurrentPage.equals(hit)) { if (DEBUG.PRESENT) out("hit on current page " + mCurrentPage); doDefaultForwardAction(); // if (goForward(SINGLE_STEP)) // return true; // if (mCurrentPage.onPathway() && mCurrentPage.isMapViewNode()) { // // a click on the current map-node page: stay put // return false; // } // // hit on what what we just clicked on: backup, // // but only if it's not a full pathway entry // // (meant for intra-slide clicking) // //if (mCurrentPage.entry == null) { // // if we're non-linear nav off a pathway, revisit prior, // // OTHERWISE, if general "browse", pop the focal // if (mCurrentPage.insideSlide() || mLastPathwayPage == null) { // focusUp(e); // } // else if (mVisited.hasPrev()) { // // TODO: ONLY DO THIS IF PREVIOUS IS AN ANCESTOR OF CURRENT // // (this could be tricky to figure out using pages tho...) // // E.g., only do this if delving into a slide. // // Current undesired behaviour: clicking on a map-view // // page is auto-backing up! // // 2007-10-22 For now: changed Page.isMapViewNode to return TRUE // // if entry is null, so this code is cut off above -- the only // // place isMapViewNode() is ever used. // revisitPrior(); // } } else { if (DEBUG.PRESENT) out("default hit; focusing to: " + hit); // todo: use tobe refocus code, which ZoomTool should also // use (hell, maybe this should all go in the MapViewer) setPage(hit); } return true; } private void doDefaultForwardAction() { if (DEBUG.PRESENT) out("doDefaultForwardAction"); if (mVisited.hasPrev() && mCurrentPage.entry == null) { if (DEBUG.PRESENT) out(" CUR FOCAL: " + mFocal); if (DEBUG.Enabled && mFocal != mCurrentPage.getPresentationFocal()) Util.printStackTrace("mFocal != curPagePresFocal; " + mFocal + " != " + mCurrentPage.getPresentationFocal()); final LWComponent lastFocal = mVisited.prev().getPresentationFocal(); boolean lastWasAncestor = mFocal.hasAncestor(lastFocal); if (DEBUG.PRESENT) out("LAST FOCAL: " + lastFocal + "; ANCESTOR=" + lastWasAncestor); if (lastWasAncestor) { revisitPrior(); return; } } if (goForward(SINGLE_STEP)) { if (DEBUG.PRESENT) out("went forward"); } else if (mLastPathwayPage != null && mCurrentPage.entry == null) { // final default: if we have a last pathway page, just go there setPage(mLastPathwayPage); } // if (goForward(SINGLE_STEP)) // return; // if (mCurrentPage.onPathway() && mCurrentPage.isMapViewNode()) { // // a click on the current map-node page: stay put // return false; // } // if (mVisited.hasPrev() && mCurrentPage.getPresentationFocal().hasAncestor(mVisited.prev().getPresentationFocal())) // revisitPrior(); // // hit on what what we just clicked on: backup, // // but only if it's not a full pathway entry // // (meant for intra-slide clicking) // //if (mCurrentPage.entry == null) { // // if we're non-linear nav off a pathway, revisit prior, // // OTHERWISE, if general "browse", pop the focal // if (mCurrentPage.insideSlide() || mLastPathwayPage == null) { // focusUp(e); // } // else if (mVisited.hasPrev()) { // // TODO: ONLY DO THIS IF PREVIOUS IS AN ANCESTOR OF CURRENT // // (this could be tricky to figure out using pages tho...) // // E.g., only do this if delving into a slide. // // Current undesired behaviour: clicking on a map-view // // page is auto-backing up! // // 2007-10-22 For now: changed Page.isMapViewNode to return TRUE // // if entry is null, so this code is cut off above -- the only // // place isMapViewNode() is ever used. // } } private boolean checkForNavNodeClick(MapMouseEvent e) { if (!mShowNavNodes) return false; Page hitPage = null; for (NavNode nav : mNavNodes) { if (DEBUG.PICK) out("pickCheck " + nav + " point=" + e.getPoint() + " mapPoint=" + e.getMapPoint()); hitPage = nav.hitPage(e.getX(), e.getY()); if (hitPage != null) { if (DEBUG.PRESENT) out("HIT PAGE " + hitPage + "; on NavNode: " + nav); break; } } if (hitPage != null) { if (hitPage.equals(mVisited.prev())) { revisitPrior(); } else { setPage(hitPage); } return true; } else return false; // if (nav.containsLocalCoord(e.getX(), e.getY())) { // if (DEBUG.PRESENT) System.out.println("HIT " + nav); // if (nav.page.equals(mVisited.prev())) { // revisitPrior(); // } else { // setPage(nav.page); // } // return true; // } // } // return false; } @Override public void handleFullScreen(boolean entering, boolean nativeMode) { ResumeButton.setVisible(!entering || !nativeMode); // out("handleFullScreen: " + entering + " native=" + nativeMode); // if (entering) { // //if (nativeMode) // VueAction.setAllActionsIgnored(true); // } else // VueAction.setAllActionsIgnored(false); } @Override public void handleToolSelection(boolean selected, VueTool fromTool) { //handleFullScreen(selected && VUE.inFullScreen(), VUE.inNativeFullScreen()); VueAction.setAllActionsIgnored(selected); if (!selected) { ResumeButton.setVisible(false); return; } ExitButton.setVisible(true); ZoomButton.setVisible(true); //GUI.refreshGraphicsInfo(); //MouseRightActivationPixel = GUI.GScreenWidth - 40; //MouseRightClearAfterActivationPixel = GUI.GScreenWidth - 200; mCurrentPage = NO_PAGE; if (VUE.getSelection().size() == 1) mNextPage = VUE.getSelection().first(); else mNextPage = null; VUE.getSelection().clear(); //repaint(); } public void startPresentation() { startPresentation(VUE.getSelection()); } private boolean startUnderway; public void startPresentation(LWSelection selection) { // TODO: start pre-caching for all image content in the presentation, or, perhaps, only for // the next slide? E.g., walk slide / slides / entries looking for LWImages and asking // them to pre-cache (which they'll just hand off to the ImageRef's). This process should // STOP should we ever see an OutOfMemory error, or, even, if we're getting close to maximum // memory usage. out("startPresentation"); //new Throwable("FYI: startPresentation (debug)").printStackTrace(); //if (DEBUG.PRESENT && DEBUG.META) tufts.Util.printStackTrace("startPresentation"); //mShowContext.setSelected(false); mVisited.clear(); mLastPage = NO_PAGE; mLastSlidePage = null; mPathway = null; mStartPathway = null; mLastPathwayPage = null; mLastStartPathwayPage = null; final LWPathway pathway = pickStartingPathway(selection); mStartPathway = pathway; startUnderway = true; // if (pathway != null && !Images.lowMemoryConditions()) { // // If running really low on memory, this might make a presentation worse. // pathway.preCacheContent(); // } try { if (pathway != null && pathway.length() > 0) { final LWPathway.Entry entry = pathway.getCurrentEntry(); if (entry != null && !entry.isPathway()) { setEntry(entry); } else { setEntry(pathway.getEntry(0)); } } else { loadPathway(null); //mPathwayIndex = 0; if (selection.size() > 0) setPage(VUE.getSelection().first()); // else if (mCurrentPage != NO_PAGE) // won't have any effect! // setPage(mCurrentPage); // else // setPage(mNextPage); } } finally { startUnderway = false; } if (DEBUG.Enabled) out("startPresentation: completed"); } private LWPathway pickStartingPathway(LWSelection selection) { final LWComponent singleSelect = selection.only(); LWPathway pathway = null; if (singleSelect != null) { Log.debug("FOUND SINGLE SELECT: " + singleSelect); pathway = singleSelect.getExclusiveVisiblePathway(); Log.debug("FOUND EXCLUSIVE VISIBLE PATHWAY: " + pathway); } else { Log.debug("NO SINGLE SELECT IN: " + selection); } // Could ALSO do: if the current single select is on the current ACTIVE pathway, // use that as the starting point, tho I think we may already have that, as the // currently active pathway should already have the current single-select as the // active entry. if (pathway != null) { // single-selection of a node with a single visible pathway entry // overrides current pathway status. pathway.setIndex(pathway.firstIndexOf(singleSelect)); VUE.setActive(LWPathway.class, this, pathway); } else { // Use the currently active pathway, and force it to be visible // if it currently isn't. pathway = VUE.getActivePathway(); if (pathway != null && !pathway.isVisible()) { if (!pathway.isLocked() && pathway.hasEntries()) pathway.setVisible(true); else pathway = null; // non-pathway presentation mode } } return pathway; } /** @param direction either FORWARD or BACKWARD */ private LWPathway.Entry nextPathwayEntry(String direction, boolean allTheWay) { if (mLastPathwayPage == null) return null; final Entry entry = mLastPathwayPage.entry; if (direction == FORWARD) { if (allTheWay) return entry.pathway.getLast(); else return entry.next(); } else { // direction == BACKWARD if (allTheWay) return entry.pathway.getFirst(); else return entry.prev(); } } //private LWComponent guessNextPage() { return null; } private void setEntry(LWPathway.Entry e) { setEntry(e, RECORD_BACKUP); } //private Entry mNextEntryToCache; /** this will jump us to the pathway for the entry, and set us viewing the given entry */ private void setEntry(LWPathway.Entry entry, boolean recordBackup) { //out("setEntry " + entry); if (entry == null) return; setPage(new Page(entry), recordBackup); //mNextEntryToCache = entry.next(); } private void loadPathway(LWPathway pathway) { //if (DEBUG.PRESENT) new Throwable("FYI, loadPathway: " + pathway).printStackTrace(); if (DEBUG.PRESENT) out("loadPathway " + pathway); LWComponent.swapLWCListener(this, mPathway, pathway); mPathway = pathway; // if (pathway != null) // preCacheContent(pathway); } public void LWCChanged(LWCEvent e) { // if the pathway changes it's filtering state, we'll get this event from it to know to repaint if (e.key == LWKey.Repaint || e.source instanceof LWMap) repaint(e.toString()); } public void out(String s) { if (DEBUG.Enabled) Log.debug(s); } private void recordPageTransition(final Page page, boolean recordBackup) { if (DEBUG.WORK||DEBUG.PRESENT) { if (DEBUG.META) Util.printStackTrace("recordPageTransition"); System.out.println("\n-----------------------------------------------------------------------------"); out(" mCurrentPage: " + mCurrentPage); out("pageTransition: " + page); out(" mVisited.prev: " + mVisited.prev()); } if (page == null) // for now return; if (page.entry != null && page.entry.isPathway()) { Util.printStackTrace("recordPageTransition to master: " + page.entry); return; } // we're backing up: if (page.equals(mVisited.prev())) { // [too aggressive: if click again ] // ANY time we record a page transtion to what's one back on // the queue, treat it as a rollback, even if recordBackup is true. // This may be too agressive, but it's worth a try for now. if (DEBUG.PRESENT) out("ROLLBACK"); mVisited.rollBack(); } else if (recordBackup) { if (page.isMap()) ; // don't record map views else mVisited.push(page); } // if (recordBackup) { // mVisited.push(page); // } else { // // we're backing up: // //System.out.println("\nCOMPARING:\n\t" + page + "\n\t" + mVisited.prev()); // if (page.equals(mVisited.prev())) // mVisited.rollBack(); // } mLastPage = mCurrentPage; mCurrentPage = page; if (page.getPresentationFocal() instanceof LWSlide) mLastSlidePage = page; if (page.onPathway()) { if (page.pathway() != mPathway) loadPathway(page.pathway()); //if (!VUE.inNativeFullScreen()) // don't send any events just in case VUE.setActive(LWPathway.Entry.class, this, page.entry); mLastPathwayPage = page; if (page.pathway() == mStartPathway && mStartPathway != null) { // // this now only ever changes if it was the pathway we were // // on when the presentation started (the active pathway then) // // SMF as per Melanie 10/29/07 mLastStartPathwayPage = page; } } mNextPage = null; mNavNodes.clear(); } private void setPage(LWComponent destination) { if (destination != null) setPage(new Page(destination), RECORD_BACKUP); } private void setPage(Page page) { setPage(page, RECORD_BACKUP); } private volatile boolean pageLoadingUnderway = false; private void setPage(final Page page, boolean recordBackup) { if (page == null) { // for now Util.printStackTrace("null page!"); return; } if (page.equals(mCurrentPage)) { if (DEBUG.Enabled) Log.debug("current page matches new page: " + mCurrentPage + "; requested=" + page); return; } if (DEBUG.PRESENT) out("setPage " + page); if (page.entry != null && page.entry.isPathway()) { Log.warn("attempt to present master slide denied: " + page.entry); if (DEBUG.Enabled) Util.printStackTrace("present master slide attempt: " + page.entry); return; } recordPageTransition(page, recordBackup); // final boolean doSlideTransition; // if (mCurrentPage != null && mCurrentPage.entry != null && page.entry != null) // doSlideTransition = true; // else // doSlideTransition = false; final MapViewer viewer = VUE.getActiveViewer(); viewer.clearTip(); // just in case pageLoadingUnderway = true; try { // the viewer will shortly call us right back with handleFocalSwitch: viewer.switchFocal(page.getPresentationFocal()); } finally { pageLoadingUnderway = false; } /* if (doSlideTransition && mFadeEffect) { // It case there was a tip visible, we need to make sure // we wait for it to finish clearing before we move on, so // we need to put the rest of this in the queue. (if we // don't do this, the screen fades out & comes back before // the map has panned) VUE.invokeAfterAWT(new Runnable() { public void run() { if (mFadeEffect) makeInvisible(); zoomToFocal(page.getPresentationFocal(), false); //zoomToFocal(page.getPresentationFocal(), !mFadeEffect); if (mScreenBlanked) makeVisibleLater(); } }); } else { final boolean animate; // Figure out if this transition is across a continuous coordinate // region (so we can animate). E.g., we're moving across the map, // or within a single slide. Slides and the map they're a part of // exist in separate coordinate spaces, and we can't animate // across that boundary. final LWComponent oldFocal = mLastPage.getPresentationFocal(); final LWComponent newFocal = mCurrentPage.getPresentationFocal(); final LWComponent oldParent = oldFocal == null ? null : oldFocal.getParent(); final LWComponent newParent = newFocal.getParent(); final LWComponent lastSlideAncestor = oldFocal == null ? null : oldFocal.getAncestorOfType(LWPathway.class); final LWComponent thisSlideAncestor = newFocal.getAncestorOfType(LWPathway.class); if (oldParent == newParent || lastSlideAncestor == thisSlideAncestor) { animate = true; //if (oldParent == newParent && newParent instanceof LWMap && !(newFocal instanceof LWPortal)) { if (false && oldParent == newParent && newParent instanceof LWMap && !(newFocal instanceof LWPortal)) { // temporarily load the map as the focal so we can // see the animation across the map if (DEBUG.WORK) out("loadFocal of parent for x-zoom: " + newParent); viewer.loadFocal(newParent); } } else animate = false; //GUI.invokeAfterAWT(new Runnable() { public void run() { // don't invoke later: is allowing an overview map repaint in an intermediate state zoomToFocal(newFocal, animate); //}}); } */ } private Rectangle2D.Float getFocalBounds(LWComponent c) { return c.getFocalBounds(); //return MapViewer.getFocalBounds(c); } // TODO: if we're currently animating a focal swith, // abort it -- and sycnrhonized as needed so as to // never have the focal be in an intermediate state @Override public boolean handleFocalSwitch(final MapViewer viewer, final LWComponent oldFocal, final LWComponent newFocal) { if (DEBUG.PRESENT) out("handleFocalSwitch in " + viewer + "\n\tfrom: " + oldFocal + "\n\t to: " + newFocal); if (!pageLoadingUnderway) recordPageTransition(new Page(newFocal), true); // final boolean isSlideTransition; // if (mCurrentPage != null && mLastPage.entry != null && mCurrentPage.entry != null) // isSlideTransition = true; // else // isSlideTransition = false; mFocal = mCurrentPage.getPresentationFocal(); // MapViewer auto-animate will only work if the new focal // is the map (it loads the focal, then does the anmiated transition). // (this is a pop-focal) // If we want to re-enable zoom-in animations, the below commented out code will // need to be cleaned up to compute the animating focal, tho this should be // simpler now that slide's are on the map as slide icons. final boolean animate = newFocal instanceof LWMap; if (mFadeEffect && !animate) { if (!startUnderway) makeInvisible(); viewer.loadFocal(mFocal, FIT_FOCAL, NO_ANIMATE); if (!startUnderway) fadeUpLater(); } else { viewer.loadFocal(mFocal, FIT_FOCAL, animate); } if (true) return true; //----------------------------------------------------------------------------- // // Below code is currently all dead... // //----------------------------------------------------------------------------- // if (isSlideTransition && mFadeEffect) { // // It case there was a tip visible, we need to make sure // // we wait for it to finish clearing before we move on, so // // we need to put the rest of this in the queue. (if we // // don't do this, the screen fades out & comes back before // // the map has panned) // VUE.invokeAfterAWT(new Runnable() { // public void run() { // makeInvisible(); // mFocal = mCurrentPage.getPresentationFocal(); // viewer.loadFocal(mFocal, true, false); // //zoomToFocal(page.getPresentationFocal(), false); // //zoomToFocal(page.getPresentationFocal(), !mFadeEffect); // if (mScreenBlanked) // makeVisibleLater(); // } // }); // return true; // } if (startUnderway) { mFocal = newFocal; if (DEBUG.PRESENT) out("handleFocalSwitch: starting focal " + newFocal); viewer.loadFocal(newFocal, FIT_FOCAL, NO_ANIMATE); return true; } // Figure out if this transition is across a continuous coordinate // region (so we can animate). E.g., we're moving across the map, // or within a single slide. Slides and the map they're a part of // exist in separate coordinate spaces, and we can't animate // across that boundary. // final LWComponent oldParent = oldFocal == null ? null : oldFocal.getParent(); // final LWComponent newParent = newFocal.getParent(); // final LWComponent lastSlideAncestor = oldFocal == null ? null : oldFocal.getAncestorOfType(LWSlide.class); // final LWComponent thisSlideAncestor = newFocal.getAncestorOfType(LWSlide.class); if (AnimateTransitions) { LWComponent animatingFocal = null; // a TEMPORARY focal to animate across // if (lastSlideAncestor == thisSlideAncestor && thisSlideAncestor != null) // animatingFocal = thisSlideAncestor; // else if (oldFocal.hasAncestor(newFocal)) { animatingFocal = newFocal; } else if (newFocal.hasAncestor(oldFocal)) { animatingFocal = oldFocal; } else if (oldFocal instanceof LWSlide || newFocal instanceof LWSlide) animatingFocal = newFocal.getMap(); // if (oldFocal instanceof LWSlide || newFocal instanceof LWSlide) // animatingFocal = newFocal.getMap(); // else if (oldFocal.hasAncestor(newFocal)) // animatingFocal = newFocal; boolean loaded = false; if (animatingFocal != null) { // if the new focal is a parent of old focal, first // load it (without auto-zooming), so we can pan across // it while we animate viewer.loadFocal(animatingFocal, false, false); mFocal = newFocal; if (animatingFocal == newFocal) loaded = true; // now zoom to the current focal within the new parent focal: this should // have the effect of making the parent focal visible, while the viewer is // actually in the same place on the old focal within the new focal: (todo: // this would actually be good default MapViewer behavior: if load the focal // of any ancestor, leave is looking at the same absolute map location, // which means adjusting our offset within the new focal) ZoomTool.setZoomFitRegion(viewer, getFocalBounds(oldFocal), oldFocal.getFocalMargin(), false); } if (DEBUG.PRESENT) out(" animating focal: " + animatingFocal); if (DEBUG.PRESENT) out("destination focal: " + newFocal); if (animatingFocal instanceof LWSlide && newFocal == animatingFocal) animateToFocal(viewer, newFocal, true); else animateToFocal(viewer, newFocal, false); } //if (oldParent == newParent || lastSlideAncestor == thisSlideAncestor) //if (!loaded) { if (true) { // just in case... GUI.invokeAfterAWT(new Runnable() { public void run() { mFocal = newFocal; // TODO: THREADSAFE? viewer.loadFocal(newFocal, true, false); }}); } return true; // if (oldParent == newParent || lastSlideAncestor == thisSlideAncestor) { // animate = true; // //if (oldParent == newParent && newParent instanceof LWMap && !(newFocal instanceof LWPortal)) { // if (false && oldParent == newParent && newParent instanceof LWMap && !(newFocal instanceof LWPortal)) { // // temporarily load the map as the focal so we can // // see the animation across the map // if (DEBUG.WORK) out("loadFocal of parent for x-zoom: " + newParent); // viewer.loadFocal(newParent); // } // } else // animate = false; // //GUI.invokeAfterAWT(new Runnable() { public void run() { // // don't invoke later: is allowing an overview map repaint in an intermediate state // animateToFocal(viewer, newFocal); // //}}); // return false; } private void animateToFocal(MapViewer viewer, LWComponent newFocal, boolean forceRawSlideBounds) { if (DEBUG.WORK) out("zoomToFocal: animating to " + newFocal); Rectangle2D focalBounds; //if (newFocal instanceof LWSlide && mFocal == newFocal) if (forceRawSlideBounds) focalBounds = newFocal.getBounds(); else focalBounds = getFocalBounds(newFocal); final boolean animate; ZoomTool.setZoomFitRegion(viewer, //getFocalBounds(newFocal), focalBounds, newFocal.getFocalMargin(), AnimateAcrossMap); } /* private void zoomToFocal(LWComponent focal, boolean animate) { final MapViewer viewer = VUE.getActiveViewer(); if (animate) { if (DEBUG.WORK) out("zoomToFocal: animating to " + focal); ZoomTool.setZoomFitRegion(viewer, focal.getBounds(), 0, animate); } if (DEBUG.WORK) out("zoomToFocal: final viewer load: " + focal); viewer.loadFocal(focal); } */ @Override public DrawContext getDrawContext(DrawContext dc) { dc.setPresenting(true); // If this is a transition along a pathway, we want to figure // that out so we can draw the pathways in the case, even // while animating (so you can see the travel along the // pathway stroke). And what we REALLY want is to only draw // the pathway we're animating along (perhaps we could do that // here manually in postDraw, or even just draw the single // connector stroke between those two nodes!) // if (mCurrentPage != null) // dc.setDrawPathways(mCurrentPage.node instanceof LWMap); //out("getDrawContext"); dc.setInteractive(false); // draw no selections, tho we depend on this being interactive right now... dc.setDrawPathways(dc.isAnimating() || mCurrentPage == null || mCurrentPage.node instanceof LWMap); //if (mShowNavNodes) dc.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.5f)); return dc; } @Override public void handlePreDraw(DrawContext dc, MapViewer viewer) { if (dc.focal instanceof LWMap || mPathway == null) { dc.fillBackground(VUE.getActiveMap().getPresentationBackgroundValue()); return; } if (mPathway != null) { if (mCurrentPage != null) { final LWSlide master = mPathway.getMasterSlide(); boolean drawMaster = false; if (DEBUG.PRESENT) out("handlePreDraw: FILLING FOR ENTRY: " + mCurrentPage.entry); if (mCurrentPage.entry != null) { dc.fillBackground(mCurrentPage.entry.getFullScreenFillColor(dc)); //if (!mCurrentPage.entry.hasSlide()) if (mCurrentPage.entry.isMapView() || !mCurrentPage.entry.hasSlide()) // For VUE-967 drawMaster = true; } else { dc.fillBackground(master.getFillColor()); // current page has no entry: we've navigated off pathway // onto an arbitrary map node: drawMaster = false; } if (drawMaster) { if (DEBUG.PRESENT) out("handlePreDraw: DRAWING MASTER " + master); master.drawFit(dc.create(), 0); //master.drawIntoFrame(dc); } } } } private boolean isOverviewVisible() { return mShowOverview && mFocal instanceof LWMap == false; } @Override public void handlePostDraw(DrawContext dc, MapViewer viewer) { if (DEBUG.PRESENT) out("handlePostDraw " + viewer + "; showNav=" + mShowNavNodes); // TODO TODO: portal's are leaving a clip in place that can // prevent nav nodes / overview map from being seen / clip // them in the middle! if (mShowNavNodes) { // TODO: Will need a set of nav nodes per-viewer if this is to work in both // the main viewer and the slide viewer... for now, the slide viewer ignores // all drawing effects of the active tool, so we don't have to worry about // it. if (true || dc.isInteractive()) { drawNavNodes(dc.push()); dc.pop(); } else out("NON-INTERACTIVE (no NavNodes)"); if (mDidAutoShowNavNodes) { mShowNavNodes = mForceShowNavNodes = false; mDidAutoShowNavNodes = false; } } // Be sure to draw the navigator after the nav nodes, as navigator // display can depend on the current nav nodes, which are created // at draw time. if (isOverviewVisible()) { drawOverviewMap(dc.push()); dc.pop(); } if (true || dc.isInteractive()) { if (ResumeButton.isVisible()) { dc.setFrameDrawing(); ResumeButton.setLocation(30, dc.frame.height - (ResumeButton.height+70)); ResumeButton.draw(dc); } else if (ExitButton.isVisible()) { dc.setFrameDrawing(); // only really need to set location when this tool actives, but just // in case the screen size should change... ExitButton.setLocation(30, dc.frame.height - (ExitButton.height+20)); ExitButton.draw(dc); } else if (ZoomButton.isVisible()){ dc.setFrameDrawing(); // only really need to set location when this tool actives, but just // in case the screen size should change... ZoomButton.setLocation(dc.frame.width-80, dc.frame.height - (ZoomButton.height+20)); ZoomButton.draw(dc); } } if (DEBUG.NAV) { dc.setFrameDrawing(); //dc.g.translate(dc.frame.x, dc.frame.y); //dc.g.setFont(VueConstants.FixedFont); dc.g.setFont(new Font("Lucida Sans Typewriter", Font.BOLD, 10)); //dc.g.setColor(new Color(128,128,128,192)); dc.g.setColor(Color.green); int y = 10; dc.g.drawString(" Frame: " + tufts.Util.out(dc.frame), 10, y+=15); dc.g.drawString(" Page: " + mCurrentPage, 10, y+=15); dc.g.drawString(" OnPath: " + onCurrentPathway(), 10, y+=15); dc.g.drawString("StartPathway: " + mStartPathway, 10, y+=15); dc.g.drawString("LstStrtPPage: " + mLastStartPathwayPage, 10, y+=15); dc.g.drawString(" LastPathway: " + mPathway, 10, y+=15); dc.g.drawString("LastPathPage: " + mLastPathwayPage, 10, y+=15); dc.g.drawString("CurPageFocal: " + mCurrentPage.getPresentationFocal(), 10, y+=15); y+=5; dc.g.setFont(new Font("Lucida Sans Typewriter", Font.BOLD, 10)); int i = 0; for (Page p : mVisited) { if (p == mVisited.current()) dc.g.setColor(Color.green); else dc.g.setColor(Color.gray); dc.g.drawString(String.format("%2d %s", i, p), 10, y+=15); i++; } //dc.g.drawString(" Backup: " + mVisited.peek(), 10, y+=15); //dc.g.drawString("BackNode: " + mVisited.peekNode(), 10, y+=15); } // if (Images.lowMemoryConditions()) { // // todo: if we ever get a second EOM while attempting local-caching, // // we may want to then abandon pathway-local caching entirely. // attemptPathwayLocalPreCaching(); // } // This is now the ONLY caching we attempt: // We also try and delay it a bit (AWT EDT task) so if the Images cache is // running in LIFO mode, the next slide contents will not override the loads for // the current slide. We'd really need a separate Images API entry point for // caching to specity low-priority tasks for this to work perfectly. GUI.invokeAfterAWT(new Runnable() { public void run() { attemptPathwayLocalPreCaching(); }}); } private Entry mLastCacheCheckedEntry = null; /** try and pre-cache slides near us in the presentation */ private void attemptPathwayLocalPreCaching() { // if we're low memory, at least try and cache the next (or prev) slide's // content. If we're REALLY low in memory, this may actually be a bad idea -- // would be best to obtain hard image locks for all content on the current page // first before doing this, as we don't want anything currently on the screen to // be GC'd in service to anything else, tho as long as there's no repaint, it'll // still be in the graphics buffer. // NOTE: sometimes the entry is the pathway itself, whose // pre-cache loads ALL pathway data, so that was disabled in LWPatyway for now. final Entry thisEntry = mCurrentPage.entry; if (mLastCacheCheckedEntry == thisEntry && thisEntry != null) { // don't repeat this just because we repainted return; } mLastCacheCheckedEntry = thisEntry; final Entry lastEntry = mLastPage.entry; Entry cachingEntry = null; if (lastEntry != null && thisEntry != null && lastEntry != thisEntry) { if (lastEntry.index() < thisEntry.index()) { cachingEntry = thisEntry.next(); // moving forward //Log.debug("CACHING FORWARD CONTENT " + cachingEntry); } else { cachingEntry = thisEntry.prev(); // moving backward //Log.debug("CACHING BACKWARD CONTENT " + cachingEntry); } } else if (thisEntry != null) { cachingEntry = thisEntry.next(); //Log.debug("DEFAULTING TO FORWARD CACHING " + cachingEntry); } if (cachingEntry != null) { final LWComponent focal = cachingEntry.getFocal(); if (focal != null) { // IMAGE CACHING PERFORMANCE: ideally, even the cache-request sub-queue // would support two further sub-priorities, allowing an initial map // load to an ultra-low priority FIFO queue, and a runtime LIFO queue // for calls relevant to immediate interactive as beow. // Note that there's a tradeoff as to doing this here as well -- under // *extreme* low-memory conditions, pre-caching the next slide could // cause images on the current slide to drop down from full-resolution // to icon-resolution as they're GC'd to make room for images on the // next slide. An even fancier image architecture could allow for // temorarily locking the current slide images into memory so they // couldn't be GC'd. // Note also tho that under low-memory conditions, the REPEATED calls to // this may be helpful, as even images we've already seen may have, of // course, been GC'd, and re-requesting them to be cached is releveant. GUI.invokeAfterAWT(new Runnable() { public void run() { focal.preCacheContent(); }}); } } } private void drawNavNodes(DrawContext dc) { LWComponent node = mCurrentPage.getOriginalMapNode(); mNavNodes.clear(); // if (node == null || (node.getLinks().size() == 0 && node.getPathways().size() < 2)) // return; makeNavNodes(dc.getFrame()); if (DEBUG.PRESENT) out("drawing nav nodes " + mNavNodes.size()); dc.setDrawPathways(false); dc.setInteractive(false); for (LWComponent c : mNavNodes) { dc.setFrameDrawing(); // reset the GC each time, as draw translate it to local coords each time c.draw(dc); } } private static final int NavNodeX = -10; // clip the left edge of the round-rect private class NavLayout { static final int InsetOffEdge = -10; static final int InsetIndent = 7; static final int VerticalGap = 7; final float rightSide; int rightInset = InsetOffEdge; float y; boolean offEdgeNodes = true; final Set<LWComponent> unique = new HashSet(); //final Object skip1, skip2; //NavLayout(Rectangle frame, Object s1, Object s2) { NavLayout(Rectangle frame) { //skip1 = s1; skip2 = s2; rightSide = frame.x + frame.width; y = frame.y + VerticalGap; } void recordUnique(Page page) { if (page != null) recordUnique(page.getOriginalMapNode()); } void recordUnique(LWComponent c) { if (c != null) unique.add(c); } void startIndentRegion() { offEdgeNodes = false; y += 30; rightInset = InsetIndent; } void addIfUnique(Page page, String debug) { boolean repeat = unique.contains(page.getOriginalMapNode()); // boolean repeat = false; // for (NavNode nn : mNavNodes) { // if (page.getOriginalMapNode() == nn.page.getOriginalMapNode()) { // repeat = true; // break; // } // } //if (!page.equals(skip1) && !page.equals(skip2)) if (!repeat || DEBUG.PRESENT) { final NavNode nn = new NavNode(page, offEdgeNodes, NavNode.NORMAL, debug); add(nn); if (DEBUG.PRESENT && repeat) nn.setFillColor(null); } } void add(NavNode nn) { mNavNodes.add(nn); recordUnique(nn.page); nn.setLocation((rightSide - nn.getWidth()) - rightInset, y); y += nn.getHeight() + VerticalGap; } void setLastPathway(Page page) { //addIfUnique(page, "last-start-path"); add(new NavNode(page, offEdgeNodes, NavNode.ITALIC, "last-start-path")); } void setCurrentPage(Page page) { add(new NavNode(page, offEdgeNodes, NavNode.STANDOUT, "cur-page")); } private void add(Page page, String debug) { add(new NavNode(page, offEdgeNodes, NavNode.NORMAL, debug)); } } private void makeNavNodes(Rectangle frame) { if (DEBUG.PRESENT) out("makeNavNodes " + mCurrentPage); if (mCurrentPage == NO_PAGE || mCurrentPage == null) return; final NavLayout layout = new NavLayout(frame); // pre-load these, as they're always forced added, and want to // push any others out: //layout.recordUnique(mLastStartPathwayPage); layout.recordUnique(mCurrentPage); // // SMF 2007-10-29: back node removed as per Melanie // final Page prev = mVisited.prev(); // if (prev != null && prev != NO_PAGE && !prev.equals(mLastPathwayPage)) // layout.add(prev, "back"); final Page prev; final Entry curEntry = mCurrentPage.entry; if (curEntry != null && curEntry.prev() != null) prev = new Page(curEntry.prev()); else prev = null; // always add the current pathway at the top if (mLastStartPathwayPage != null && mLastStartPathwayPage.entry != mCurrentPage.entry) layout.setLastPathway(mLastStartPathwayPage); if (prev != null && !prev.equals(mLastStartPathwayPage) && !prev.equals(mCurrentPage)) layout.addIfUnique(prev, "path-prior"); // if (mCurrentPage.node != null && mCurrentPage.node.inVisiblePathway()) // layout.add(new Page(mCurrentPage.node), "loop-back"); layout.setCurrentPage(mCurrentPage); layout.startIndentRegion(); final LWComponent mapNode = mCurrentPage.getOriginalMapNode(); if (curEntry != null) { final Entry nextEntryThisPath = curEntry.next(); if (nextEntryThisPath != null) layout.add(new Page(nextEntryThisPath), "next-path"); } if (mapNode == null) return; for (LWPathway path : mapNode.getPathways()) { if (path.isDrawn() && path != mPathway) { LWPathway.Entry pathEntry = path.getFirstEntry(mapNode); LWPathway.Entry nextPathEntry = pathEntry.next(); if (nextPathEntry != null) layout.addIfUnique(new Page(nextPathEntry), "other-path"); } } //if (DEBUG.NAV) { /*make room for diagnostics*/ y += 200; } NavNode nav; for (LWLink link : mapNode.getLinks()) { // LWComponent farpoint = link.getFarNavPoint(mapNode); // adjust for arrow directionality LWComponent farpoint = link.getFarPoint(mapNode); if (DEBUG.WORK) out(mapNode + " found farpoint " + farpoint); if (farpoint != null && farpoint.isDrawn()) { // if (false && link.hasLabel()) // nav = createNavNode(new Page(link)); // //nav = createNavNode(link, null); // just need to set syncSource to the farpoint // else // nav = createNavNode(new Page(farpoint)); layout.addIfUnique(new Page(farpoint), "link"); // //nav = createNavNode(farpoint, null); // mNavNodes.add(nav); // y += nav.getHeight() + 5; // nav.setLocation((rightSide - nav.getWidth()) - rightInset, y); } } } public static void makeInvisible() { // TODO: Would be best to route these through full-screen code so it can ignore these // requests while in a transition to full-screen, waiting for the first paint to // happen, tho checking for startUnderway where these are called works for us now. if (Util.isMacCocoaSupported() && VUE.inNativeFullScreen()) { //out("makeInvisible"); try { // TODO: don't call unless Mac extensions available: is generating needless error log output MacOSX.makeMainInvisible(); mScreenBlanked = true; } catch (Error e) { Log.error("makeInvisible;", e); } } } public static void fadeUp() { if (Util.isMacCocoaSupported() && VUE.inNativeFullScreen()) { try { if (DEBUG.PRESENT) Log.debug("fadeUp"); //if (MacOSX.isMainInvisible()) MacOSX.fadeUpMainWindow(); mScreenBlanked = false; } catch (Error e) { Log.error("fadeUp;", e); } } } public static void fadeUpLater() { if (Util.isMacCocoaSupported() && VUE.inNativeFullScreen()) { if (DEBUG.PRESENT) Log.debug("requesting fadeUp"); VUE.invokeAfterAWT(new Runnable() { public void run() { fadeUp(); }}); } } /* // private LWComponent currentNode() { // if (mCurrentPage instanceof LWSlide) // return mEntry == null ? null : mEntry.node; // else // return mCurrentPage; // } public void handleSelectionChange(LWSelection s) { out("SELECTION CHANGE"); // TODO: if active map changes, need to be sure to clear current page! if (s.size() == 1) mCurrentPage = s.first(); // if (s.size() == 1) // mNextPage = s.first(); // else // mNextPage = null; } */ // private void followLink(LWComponent src, LWLink link) { // LWComponent linkingTo = link.getFarPoint(src); // mLastFollowed = link; // setPage(linkingTo); // } /* private LWComponent OLD_guessNextPage() { // todo: only bother with links that have component endpoints! List links = mCurrentPage.getLinks(); LWLink toFollow = null; if (links.size() == 1) { toFollow = (LWLink) mCurrentPage.getLinks().get(0); } else { Iterator i = mCurrentPage.getLinks().iterator(); while (i.hasNext()) { LWLink link = (LWLink) i.next(); if (link.getFarNavPoint(currentNode()) == null) continue; // if a link to nothing, ignore if (link != mLastFollowed) { toFollow = link; break; } } } LWComponent nextPage = null; if (toFollow != null) { mLastFollowed = toFollow; nextPage = toFollow.getFarNavPoint(currentNode()); if (nextPage.getParent() instanceof LWGroup) nextPage = nextPage.getParent(); } // TODO: each page keeps a mPrevPage, which we use instead of // the peek here to find the "default" backup. Maybe // a useful "uparrow" functionality can also make use of this? // uparrow should also go back up to the parent of the current // page if it's anything other than the map itself if (nextPage == null && !mVisited.isEmpty() && mVisited.peek() != mCurrentPage) nextPage = mVisited.peek(); return nextPage; } */ /* public void handleSelectionChange(LWSelection s) { if (s.size() == 1 && VUE.multipleMapsVisible() && VUE.getRightTabbedPane().getSelectedViewer().getMap() == VUE.getActiveMap()) { MapViewer viewer = VUE.getRightTabbedPane().getSelectedViewer(); ZoomTool.setZoomFitRegion(viewer, s.first().getBounds(), 16); } } */ /** @return false */ public boolean supportsSelection() { return false; } /** @return false */ public boolean supportsResizeControls() { return false; } /** @return false */ public boolean supportsDrag(InputEvent e) { return false; } /** @return false */ public boolean supportsDraggedSelector(MapMouseEvent e) { return false; } /** @return true */ public boolean hasDecorations() { return true; } /** @return true */ public boolean usesRightClick() { return true; } /** @return false */ public boolean supportsEditActions() { return false; } /** @return false if in native full screen */ @Override public boolean permitsToolChange() { if (VUE.inNativeFullScreen()) return false; else if (VUE.inWorkingFullScreen() && ResumeButton.isVisible()) return false; else return true; } } // Left handed original style nav-nodes: // private void makeNavNodes(Page page, Rectangle frame) // { // // always add the current pathway at the top // //if (node.inPathway(mPathway)) // if (page.onPathway(mPathway)) // mNavNodes.add(createNavNode(page)); // // tho having the order switch on the user kind of sucks... // final LWComponent mapNode = page.getOriginalMapNode(); // for (LWPathway otherPath : mapNode.getPathways()) { // //if (otherPath != mPathway && !otherPath.isFiltered()) // if (!otherPath.isFiltered() && otherPath != mPathway) // mNavNodes.add(createNavNode(new Page(otherPath.getFirstEntry(mapNode)))); // } // float x = NavNodeX, y = frame.y; // if (DEBUG.NAV) { // // make room for diagnostics: // y += 200; // } // for (NavNode nav : mNavNodes) { // y += nav.getHeight() + 5; // nav.setLocation(x, y); // } // if (!mNavNodes.isEmpty()) // y += 30; // NavNode nav; // for (LWLink link : mapNode.getLinks()) { // // LWComponent farpoint = link.getFarNavPoint(mapNode); // adjust for arrow directionality // LWComponent farpoint = link.getFarPoint(mapNode); // if (DEBUG.WORK) out(mapNode + " found farpoint " + farpoint); // if (farpoint != null && farpoint.isDrawn()) { // if (false && link.hasLabel()) // nav = createNavNode(new Page(link)); // //nav = createNavNode(link, null); // just need to set syncSource to the farpoint // else // nav = createNavNode(new Page(farpoint)); // //nav = createNavNode(farpoint, null); // mNavNodes.add(nav); // y += nav.getHeight() + 5; // nav.setLocation(x, y); // } // } // /* // float spacePerNode = frame.width; // if (mShowOverview) // spacePerNode -= (frame.width / OverviewMapFraction); // spacePerNode /= mNavNodes.size(); // int cnt = 0; // float x, y; // //out("frame " + frame); // for (LWComponent c : mNavNodes) { // x = cnt * spacePerNode + spacePerNode / 2; // x -= c.getWidth() / 2; // y = frame.height - c.getHeight(); // c.setLocation(x, y); // //out("location set to " + x + "," + y); // cnt++; // } // */ // } // private NavNode createNavNode(LWPathway.Entry e) { // //if (DEBUG.WORK) out("creating nav node for page " + page); // return new NavNode(new Page(e)); // } // private NavNode createNavNode(Page page) { // //if (DEBUG.WORK) out("creating nav node for page " + page); // return new NavNode(page); // } // private NavNode createNavNode(Page page, boolean isLastPathwayPage) { // //if (DEBUG.WORK) out("creating nav node for page " + page); // return new NavNode(page, isLastPathwayPage); // } // private NavNode createNavNode(LWPathway path) { // //if (DEBUG.WORK) out("creating nav node for page " + page); // return new NavNode(new Page(path.asEntry()), 300); // } // private NavNode createNavNode(LWComponent src, LWPathway pathway) { // return new NavNode(src, pathway); // } // @Override // public boolean handleFocalSwitch(final MapViewer viewer, // final LWComponent oldFocal, // final LWComponent newFocal) // { // if (DEBUG.PRESENT) out("handleFocalSwitch in " + viewer // + "\n\tfrom: " + oldFocal // + "\n\t to: " + newFocal); // if (!pageLoadingUnderway) // recordPageTransition(new Page(newFocal), true); // final boolean isSlideTransition; // if (mCurrentPage != null && mLastPage.entry != null && mCurrentPage.entry != null) // isSlideTransition = true; // else // isSlideTransition = false; // if (isSlideTransition && mFadeEffect) { // makeInvisible(); // mFocal = mCurrentPage.getPresentationFocal(); // viewer.loadFocal(mFocal, FIT_FOCAL, NO_ANIMATE); // makeVisibleLater(); // return true; // } // // if (isSlideTransition && mFadeEffect) { // // // It case there was a tip visible, we need to make sure // // // we wait for it to finish clearing before we move on, so // // // we need to put the rest of this in the queue. (if we // // // don't do this, the screen fades out & comes back before // // // the map has panned) // // VUE.invokeAfterAWT(new Runnable() { // // public void run() { // // makeInvisible(); // // mFocal = mCurrentPage.getPresentationFocal(); // // viewer.loadFocal(mFocal, true, false); // // //zoomToFocal(page.getPresentationFocal(), false); // // //zoomToFocal(page.getPresentationFocal(), !mFadeEffect); // // if (mScreenBlanked) // // makeVisibleLater(); // // } // // }); // // return true; // // } // if (startUnderway) { // mFocal = newFocal; // if (DEBUG.PRESENT) out("handleFocalSwitch: starting focal " + newFocal); // viewer.loadFocal(newFocal, FIT_FOCAL, NO_ANIMATE); // return true; // } // // Figure out if this transition is across a continuous coordinate // // region (so we can animate). E.g., we're moving across the map, // // or within a single slide. Slides and the map they're a part of // // exist in separate coordinate spaces, and we can't animate // // across that boundary. // // final LWComponent oldParent = oldFocal == null ? null : oldFocal.getParent(); // // final LWComponent newParent = newFocal.getParent(); // // final LWComponent lastSlideAncestor = oldFocal == null ? null : oldFocal.getAncestorOfType(LWSlide.class); // // final LWComponent thisSlideAncestor = newFocal.getAncestorOfType(LWSlide.class); // if (AnimateTransitions) { // LWComponent animatingFocal = null; // a TEMPORARY focal to animate across // // if (lastSlideAncestor == thisSlideAncestor && thisSlideAncestor != null) // // animatingFocal = thisSlideAncestor; // // else // if (oldFocal.hasAncestor(newFocal)) { // animatingFocal = newFocal; // } else if (newFocal.hasAncestor(oldFocal)) { // animatingFocal = oldFocal; // } else if (oldFocal instanceof LWSlide || newFocal instanceof LWSlide) // animatingFocal = newFocal.getMap(); // // if (oldFocal instanceof LWSlide || newFocal instanceof LWSlide) // // animatingFocal = newFocal.getMap(); // // else if (oldFocal.hasAncestor(newFocal)) // // animatingFocal = newFocal; // boolean loaded = false; // if (animatingFocal != null) { // // if the new focal is a parent of old focal, first // // load it (without auto-zooming), so we can pan across // // it while we animate // viewer.loadFocal(animatingFocal, false, false); // mFocal = newFocal; // if (animatingFocal == newFocal) // loaded = true; // // now zoom to the current focal within the new parent focal: this should // // have the effect of making the parent focal visible, while the viewer is // // actually in the same place on the old focal within the new focal: (todo: // // this would actually be good default MapViewer behavior: if load the focal // // of any ancestor, leave is looking at the same absolute map location, // // which means adjusting our offset within the new focal) // ZoomTool.setZoomFitRegion(viewer, // getFocalBounds(oldFocal), // oldFocal.getFocalMargin(), // false); // } // if (DEBUG.PRESENT) out(" animating focal: " + animatingFocal); // if (DEBUG.PRESENT) out("destination focal: " + newFocal); // if (animatingFocal instanceof LWSlide && newFocal == animatingFocal) // animateToFocal(viewer, newFocal, true); // else // animateToFocal(viewer, newFocal, false); // } // //if (oldParent == newParent || lastSlideAncestor == thisSlideAncestor) // //if (!loaded) { // if (true) { // just in case... // GUI.invokeAfterAWT(new Runnable() { public void run() { // mFocal = newFocal; // TODO: THREADSAFE? // viewer.loadFocal(newFocal, true, false); // }}); // } // return true; // // if (oldParent == newParent || lastSlideAncestor == thisSlideAncestor) { // // animate = true; // // //if (oldParent == newParent && newParent instanceof LWMap && !(newFocal instanceof LWPortal)) { // // if (false && oldParent == newParent && newParent instanceof LWMap && !(newFocal instanceof LWPortal)) { // // // temporarily load the map as the focal so we can // // // see the animation across the map // // if (DEBUG.WORK) out("loadFocal of parent for x-zoom: " + newParent); // // viewer.loadFocal(newParent); // // } // // } else // // animate = false; // // //GUI.invokeAfterAWT(new Runnable() { public void run() { // // // don't invoke later: is allowing an overview map repaint in an intermediate state // // animateToFocal(viewer, newFocal); // // //}}); // // return false; // }