/* * 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.ui; import java.awt.Color; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import javax.swing.AbstractButton; import javax.swing.Box; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JPanel; import javax.swing.JToggleButton; import tufts.vue.DEBUG; import tufts.vue.DrawContext; import tufts.vue.LWCEvent; import tufts.vue.LWComponent; import tufts.vue.LWMap; import tufts.vue.LWPathway; import tufts.vue.LWSelection; import tufts.vue.LWSlide; import tufts.vue.VUE; import tufts.vue.VueResources; import tufts.vue.gui.Widget; // TWO CASES TO HANDLE: // 1 - focal is slide: can we get MapViewer to handle displaying just an LWContainer? Undo will still need to point // to the original map... Note that LWSlide's behind each node aren't really in the hierarchy (maybe put them there?) // Reminder: the problem with trying to have the children in the slide be the *same object* somehow as the children // on the main map is that all the info needs to be different EXCEPT the meta-data (x,y,style,etc). // We could do that only if there was a "raw" node of just meta-data, including resource and it's meta-data, // and then multiple styled renderings of that "raw node". In practice, the only data usually in this raw node // would be a single string, for the label, and a resource. (Tho may have notes, user assigned meta-data, etc). // For SYNCING, will want to sync all meta-data (maybe both nodes reference the same meta-data objcect?) // 2 - focal is existing node in map: need MapViewer to both only DRAW this node, but prevent // selection of nearby now-"invisible" objects (in the slideviewer). Maybe disallow all clicks // out side the node? // TODO: switch off features one by one fore SlideViewer/NodeViewer, and once done that, // can use the switch marks to manage the change to a simpler superclass / fancier // full map viewing subclass if you like... // SLIDES STILL NEED TO BE OWNED BY MAP (for getnextuniqeID, persistance, UNDO), // but we can't just have them marked hidden, or they won't draw in their // own context... perhaps put them in a different layer? If we // had them in a different layer, if we wanted, that could be the "displayed" map: // the map with the slides scattered about, tho don't know if that would really be useful... // Okay, so they can be owned by the map, but could just have bit for now that says "invisible", // if we can handle the direct viewing, drawing & selecting of a container with clever traversals. public class SlideViewer extends tufts.vue.MapViewer { //private final VueTool PresentationTool = VueToolbarController.getController().getTool("viewTool"); private boolean isMapView = false; private boolean mBlackout = false; private boolean mZoomBorder; private boolean inFocal; //private LWComponent mZoomContent; // what we zoom-to private LWPathway.Entry mLastLoad; private final AbstractButton btnLocked; private final AbstractButton btnZoom; private final AbstractButton btnFocus; private final AbstractButton btnSlide; private final AbstractButton btnMaster; private final AbstractButton btnMapView; private final AbstractButton btnFill; private final AbstractButton btnRebuild; private final AbstractButton btnRevert; //private final AbstractButton btnPresent; private static final String lblRevertOnSlide = VueResources.getString("button.resetstyle.label"); private static final String lblRevertOnMaster = VueResources.getString("button.resetallstyle.label"); private boolean masterJustPressed; private boolean slideJustPressed; private class Toolbar extends JPanel implements ActionListener { Toolbar() { //btnLocked.setFont(VueConstants.FONT_SMALL); //add(btnLocked); add(Box.createHorizontalGlue()); //add(btnZoom); //if (DEBUG.Enabled) add(btnFocus); add(btnSlide); add(btnMaster); //add(btnMapView); //add(btnFill); //add(btnPresent); add(btnRebuild); add(btnRevert); ButtonGroup exclusive = new ButtonGroup(); exclusive.add(btnZoom); if (DEBUG.Enabled) exclusive.add(btnFocus); exclusive.add(btnSlide); exclusive.add(btnMaster); //exclusive.add(btnPresent); btnRebuild.setEnabled(false); btnRevert.setEnabled(false); } private void add(AbstractButton b) { b.addActionListener(this); super.add(b); } public void actionPerformed(ActionEvent e) { if (DEBUG.PRESENT) out(e); if (e.getSource() == btnMapView) { if (mLastLoad != null && !mLastLoad.isPathway()) mLastLoad.setMapView(btnMapView.isSelected()); reload(); } else if (e.getSource() == btnMaster) { masterJustPressed = true; reload(); } else if (e.getSource() == btnSlide) { slideJustPressed = true; reload(); } else if (e.getSource() == btnRebuild) { mLastLoad.rebuildSlide(); mLastLoad.pathway.getMap().getUndoManager().mark(btnRebuild.getText()); reload(); } else if (e.getSource() == btnRevert) { if (btnMaster.isSelected()) { //out("REVERTING ALL IN " + mLastLoad.pathway); for (LWPathway.Entry entry : mLastLoad.pathway.getEntries()) { //out("REVERTING ENTRY " + entry); LWSlide slide = entry.getSlide(); if (slide != null) slide.revertToMasterStyle(); } } else { //out("REVERTING CURRENT ENTRY " + mLastLoad); mLastLoad.revertSlideToMasterStyle(); } mLastLoad.pathway.getMap().getUndoManager().mark(btnRevert.getText()); } } } public SlideViewer(LWMap map) { super(null, "SLIDE"); //super(map == null ? new LWMap("empty") : map, "SlideViewer"); setName("Viewer"); // for DockWindow title and backward compat with window saved location btnLocked = new JCheckBox(VueResources.getString("checkbox.lock.label")); //btnLocked = makeButton("Lock"); btnZoom = makeButton(VueResources.getString("button.zoom.label")); btnFocus = makeButton(VueResources.getString("button.focus.label")); btnSlide = makeButton(VueResources.getString("button.slide..label")); btnMaster = makeButton(VueResources.getString("checkbox.masterslide.label")); btnMapView = new JCheckBox(VueResources.getString("checkbox.mapview.label")); btnFill = new JCheckBox(VueResources.getString("checkbox.fill.label")); btnRebuild = new JButton(VueResources.getString("button.revert.label")); btnRevert = new JButton(VueResources.getString("button.resetstyle.label")); //btnPresent = makeButton("Present"); btnSlide.setSelected(true); //DEBUG_TIMER_ROLLOVER = false; //VUE.addActiveListener(LWPathway.Entry.class, this); /* VUE.addActiveListener(LWPathway.Entry.class, new VUE.ActiveListener() { public void activeChanged(VUE.ActiveEvent e) { load((LWPathway.Entry) e.active); } }); */ /* VUE.ActivePathwayEntryHandler.addListener(new VUE.ActiveListener<LWPathway.Entry>() { public void activeChanged(VUE.ActiveEvent<LWPathway.Entry> e) { load(e.active); } }); */ } public void activeChanged(tufts.vue.ActiveEvent e, LWPathway.Entry entry) { // todo: if entry is null, leave the current contents, but NOT // if this is because the map has been closed (will want // to be tracking all active instances of LWMap to make this easy -- // e.g., if this entry's map is in the set, we're good). load(entry); } public void showSlideViewer() { if (Widget.isHidden(this) || !Widget.isExpanded(this)) Widget.setExpanded(this, true); } protected AbstractButton makeButton(String name) { AbstractButton b = new JToggleButton(name); b.setFocusable(false); //b.setBorderPainted(false); return b; } // protected String getEmptyMessage() { // if (mLastLoad != null && btnSlide.isSelected()) // return "Not on Pathway"; // else // return "Empty"; // } // @Override // public void addNotify() { // super.addNotify(); // getParent().add(new Toolbar(), BorderLayout.NORTH); // } @Override public void grabVueApplicationFocus(String from, ComponentEvent event) { final int id = event == null ? 0 : event.getID(); if (id == MouseEvent.MOUSE_ENTERED) out("GVAF " + from + " ignored"); else super.grabVueApplicationFocus(from, event); } @Override public void LWCChanged(LWCEvent e) { //Util.printStackTrace(e.toString()); if (DEBUG.PRESENT) out("SLIDEVIEWER LWCChanged " + e); if (e.component instanceof LWPathway) { // If we're displaying a slide for a node, and // the pathway has changed, it may be that the // node was just added to the pathway and we // need to load it's new slide (or it was // removed, and we also need to display that) reload(); } else { super.LWCChanged(e); if (true||e.getComponent() == mFocal) { fitToFocal(); } } } public void showMasterSlideMode() { out("showMasterSlideMode"); masterJustPressed = true; reload(); } public void showSlideMode() { out("showSlideMode"); slideJustPressed = true; reload(); } /* public LWComponent pickDropTarget(float mapX, float mapY, Object dropping) { PickContext pc = getPickContext(); if (dropping == null) pc.dropping = POSSIBLE_RESOURCE; // most lenient targeting if unknown else pc.dropping = dropping; return LWTraversal.PointPick.pick(pc, mapX, mapY); } */ // no longer relevant: maxLayer hack currently not in use //protected int getMaxLayer() { return inPathwaySlide ? 1 : 0; } /* protected PickContext getPickContext(float x, float y) { PickContext pc = super.getPickContext(x, y); // This makes sure we can't select the background, // and we can initiate the drag selector box on // children. if (inFocal && !btnMaster.isSelected()) pc.excluded = mFocal; // So we automatically pick inside groups // (same as auto-turning on the direct selection tool) pc.pickDepth = 1; return pc; } */ /* private LWPathway mCurrentPath; public void activeChanged(VUE.ActivePathwayEntry.Event e) { load(e.entry); // if (mCurrentPath != null)p // mCurrentPath.removeLWCListener(this); // mCurrentPath = path; // mCurrentPath.addLWCListener(this); // reload(); } */ private void reload() { // TODO: load nothing if active pathway from a different map //if (mLastLoad != null) if (mLastLoad == null) { if (VUE.getActivePathway() != null) load(VUE.getActivePathway().asEntry()); } else load(mLastLoad); } /* public void selectionChanged(LWSelection s) { super.selectionChanged(s); if (true) return; if (btnLocked.isSelected()) return; if (s.getSource() != this && s.size() == 1) { final LWComponent picked = s.first(); if (btnMaster.isSelected()) mLastLoad = picked; else if (btnSlide.isSelected()) { final LWPathway activePathway = picked.getMap().getActivePathway(); //if (true || c.getSlideForPathway(c.getMap().getActivePathway()) != null) if (activePathway != null && activePathway.contains(picked)) load(picked); else ; // do nothing for now: allows us to select non-slideworthy on map to drag into slide } else load(picked); } } */ @Override protected boolean skipAllPainting() { return VUE.inNativeFullScreen(); } protected void load(LWPathway.Entry entry) { if (VUE.inNativeFullScreen()) { // don't slow us down with our updates if we're in native full screen / presenting return; } if (DEBUG.PRESENT) out("SlideViewer: loading " + entry); mLastLoad = entry; LWComponent focal; // If no slide available, disable slide button, even if don't want it! if (entry == null) { mZoomBorder = false; //mZoomContent = null; inFocal = false; focal = null; btnMapView.setEnabled(false); btnRebuild.setEnabled(false); btnRevert.setEnabled(false); //} else if (btnMaster.isSelected() || entry.isPathway()) { } else if (masterJustPressed || (entry.isPathway() && !slideJustPressed)) { if (DEBUG.PRESENT) out("master auto-select"); btnMaster.setSelected(true); isMapView = false; focal = entry.pathway.getMasterSlide(); inFocal = true; btnRebuild.setEnabled(false); btnRevert.setEnabled(true); btnRevert.setText(lblRevertOnMaster); } else { if (DEBUG.PRESENT) out("slide auto-select"); btnSlide.setSelected(true); if (entry.isPathway()) { entry = entry.pathway.getCurrentEntry(); if (entry == null) { // pathway is empty -- TODO: handle cleanly return; } } isMapView = entry.isMapView(); btnMapView.setSelected(isMapView); btnRebuild.setEnabled(!entry.isOffMapSlide()); btnRevert.setEnabled(true); btnRevert.setText(lblRevertOnSlide); focal = entry.getFocal(); inFocal = true; } //mZoomContent = focal; masterJustPressed = slideJustPressed = false; super.loadFocal(focal); //reshapeImpl(0,0,0,0); if (DEBUG.PRESENT) out("SlideViewer: focused is now " + mFocal + " from map " + mMap); } private LWSlide getActiveMasterSlide() { if (VUE.getActiveMap() != null) return VUE.getActiveMap().getActivePathway().getMasterSlide(); else return null; } /** @return false -- a no-op -- don't allow focal popping in slide viewer */ @Override protected boolean popFocal(boolean toTopLevel, boolean animate) { return false; } // @Override // public void loadFocal(LWComponent focal, boolean fitToFocal, boolean animate) { // if (focal instanceof LWMap) { // // never load the map in the slide viewer // } else { // super.loadFocal(focal, fitToFocal, animate); // } // } @Override protected Color getBackgroundFillColor(DrawContext dc) { //return Color.white; return Color.darkGray; // this code produces "filled" slide viewer look, // tho then we can't make out the edge of the slide: // final LWPathway.Entry entry = VUE.getActiveEntry(); // if (entry != null) // return entry.getFullScreenFillColor(); // else // return Color.gray; } @Override protected void drawFocal(DrawContext dc) { //if (mLastLoad != null && mLastLoad.isMapView()) { if (mLastLoad != null && !mLastLoad.canProvideSlide()) { // have to fill first, or super.drawFocal will fill over us... // if (DEBUG.Enabled) // dc.fillBackground(Color.red); // else dc.fillBackground(getBackgroundFillColor(dc)); mLastLoad.pathway.getMasterSlide().drawFit(dc.push(), 0); dc.pop(); //mLastLoad.pathway.getMasterSlide().drawIntoFrame(dc); } super.drawFocalImpl(dc); } @Override protected void drawSelection(DrawContext dc, LWSelection s) { // Don't draw selection if its the focused component if (s.size() == 1 && s.first() == mFocal) return; super.drawSelection(dc, s); } @Override protected DrawContext getDrawContext(Graphics2D g) { DrawContext dc = super.getDrawContext(g); dc.setDrawPathways(false); //dc.setEditMode(btnMaster.isSelected()); /* dc.focal now handles this if (isMapView) { dc.isFocused = true;// turns off pathway drawing //dc.setInteractive(false); // turns off selection drawing } */ return dc; } @Override public void fireViewerEvent(int id, String cause) { if (DEBUG.PRESENT) out("fireViewerEvent <" + id + "> skipped"); } /* protected void reshapeImpl(int x, int y, int w, int h) { if (DEBUG.PRESENT) out("reshapeImpl"); zoomToContents(); } protected void zoomToContents() { if (mZoomContent == null) return; final java.awt.geom.Rectangle2D zoomBounds; if (inFocal) { // don't include any bounds due to current // state decorations, such as being no a pathway zoomBounds = mZoomContent.getShapeBounds(); //out("zoomToContents: shapeBounds=" + zoomBounds); } else { zoomBounds = mZoomContent.getBounds(); //out("zoomToContents: bounds=" + zoomBounds); } tufts.vue.ZoomTool.setZoomFitRegion(this, zoomBounds, btnFill.isSelected() ? 0 : 20, //mZoomBorder ? 20 : 0, false); out("zoomed to " + zoomBounds + " @ " + tufts.vue.ZoomTool.prettyZoomPercent(getZoomFactor())); } */ /* protected void drawMap(DrawContext dc) { if (mFocal == null) return; if (isMapView) { drawFocal(dc); } else { //out("Drawing focal " + mFocal); dc.setEditMode(btnMaster.isSelected()); mFocal.draw(dc); //drawSlide(dc); } } */ /* either draws the entire map (previously zoomed to to something to focus on), * or a fully focused part of of it, where we only draw that item. */ /* protected void drawFocal(DrawContext dc) { // TODO: need to get this working to wherever we've been "zoom fitted" to, and/or // fundamentally change all object drawing to be zero based. //drawMasterSlide(dc); out("drawing focal " + mFocal); if (isMapView) { dc.isFocused = true;// turns off pathway drawing //dc.setInteractive(false); // turns off selection drawing } // if (btnFocus.isSelected()) // dc.g.setColor(Color.black); // else // dc.g.setColor(mFocal.getMap().getFillColor()); // dc.g.fill(dc.g.getClipBounds()); final LWMap underlyingMap = mFocal.getMap(); //zoomToContents(); if (mFocal.isTranslucent() && mFocal != underlyingMap) { //out("drawing underlying map " + underlyingMap); // If our fill is in any way translucent, the underlying // map can show thru, thus we have to draw the whole map // to see the real result -- we just set the clip to // the shape of the focal. final Shape curClip = dc.g.getClip(); dc.g.setClip(mFocal.getShape()); underlyingMap.draw(dc); dc.g.setClip(curClip); } else { mFocal.draw(dc); } } */ /* protected void XsetDragger(LWComponent c) { // need to cleanup MapViewer such that overriding this actually works if (btnMaster.isSelected() && c instanceof LWSlide) return; else { out("setting dragger to: " + c); super.setDragger(c); } } */ /* protected void drawSlide(DrawContext dc) { // just drawing the master slide here only happens to work because it's exactly // the same size as the slide itself (mFocal is a slide of we're in here), and // both exist in the "ether", (not on the map), so they both happen to have the // same location (0,0), so we don't have to worry about panning around the map // to get them both to draw (like we will have to with map-based focals) -- this // really highlights that it would be nice to better DrawContext controls for // moving back and forth between "immediate"(?) mode v.s. map-mode, and perhaps // change the drawing model to always be zero based -- the translating could // happen during a draw traversal, which could be shared code with similar // translation we'd need to do via pick traversals. //drawMasterSlide(dc); // Now draw the actual slide mFocal.draw(dc); } private void drawMasterSlide(DrawContext dc) { drawMasterSlide(dc, mLastLoad.pathway.getMasterSlide(), btnFill.isSelected(), btnMaster.isSelected()); } public static void drawMasterSlide(DrawContext dc, LWSlide master, boolean fillMode, boolean editMode) { if (fillMode) { dc.g.setColor(master.getFillColor()); dc.g.fill(dc.g.getClipBounds()); } //out("drawMasterSlide: offsetX/Y: " + dc.offsetX + "," + dc.offsetY); //dc.setRawDrawing(); master.setLocation(0,0);// TODO: hack till we can lock these properties //master.setLocation(-dc.offsetX,-dc.offsetY); if (editMode) { // When editing the master, allow us to see stuff outside of it // (no need to clip); dc.setEditMode(true); master.draw(dc); } else { final Shape curClip = dc.g.getClip(); //out("curClip: " + curClip); // When just filling the background with the master, only draw // what's in the containment box dc.g.setClip(master.getBounds()); master.draw(dc); dc.g.setClip(curClip); } //Dc.setMapDrawing(); } */ /* // not relevant: we'lre not in a scroll pane protected void panScrollRegionImpl(int dx, int dy, boolean allowGrowth) { if (inFocal) return; else super.panScrollRegionImpl(dx, dy, allowGrowth); } */ /* public void keyPressed(java.awt.event.KeyEvent e) { super.keyPressed(e); if (e.isConsumed()) return; char c = e.getKeyChar(); if (c == 'b') { mBlackout = !mBlackout; repaint(); } } */ /* protected void XsetMapOriginOffsetImpl(float panelX, float panelY, boolean update) { if (inFocal) { super.setMapOriginOffsetImpl(mFocal.getX(), mFocal.getY(), update); } else { super.setMapOriginOffsetImpl(panelX, panelY, update); } } protected float XscreenToMapX(float x) { float nx = super.screenToMapX(x); out("SCREEN-X " + x + " MAP-X " + nx); return nx; } public float XgetOriginX() { if (inFocal) return mFocal.getX(); else return super.getOriginX(); } public float XgetOriginY() { if (inFocal) return mFocal.getY(); else return super.getOriginY(); } */ /* protected void drawMap(DrawContext dc) { if (mBlackout) { dc.g.setColor(Color.black); dc.setBlackWhiteReversed(true); dc.setPresenting(true); dc.setInteractive(false); } else dc.g.setColor(mMap.getFillColor()); dc.g.fill(dc.g.getClipBounds()); if (mFocused == null) mMap.draw(dc); else mFocused.draw(dc); dc.setRawDrawing(); //dc.g.setFont(VueConstants.FONT_MEDIUM); dc.g.setFont(new java.awt.Font("Apple Chancery", java.awt.Font.PLAIN, 14)); //dc.g.setColor(Color.blue); dc.g.setColor(new Color(89,130,203)); int x = -getX() + 2; int y = -getY() + 15; dc.g.drawString("Header", x, y); dc.g.drawString("Footer", x, -getY() + getHeight() - 3); } */ // public void reshape(int x, int y, int w, int h) { // out(" reshape: "+ w + " x " + h+ " "+ x + "," + y); // } /* private VueAction[] mMenuActions; public void addNotify() { super.addNotify(); mMenuActions = new VueAction[] { tufts.vue.Actions.ZoomFit, tufts.vue.Actions.ZoomActual, new VueAction("1/8 Screen") { public void act() { java.awt.GraphicsConfiguration gc = GUI.getDeviceConfigForWindow(SlideViewer.this); Rectangle screen = gc.getBounds(); slideDock.setSize(screen.width / 4, screen.height / 4); //GUI.refreshGraphicsInfo(); //slideDock.setSize(GUI.GScreenWidth / 4, GUI.GScreenHeight / 4); } }, new VueAction("1/4 Screen") { public void act() { java.awt.GraphicsConfiguration gc = GUI.getDeviceConfigForWindow(SlideViewer.this); Rectangle screen = gc.getBounds(); slideDock.setSize(screen.width / 2, screen.height / 2); //GUI.refreshGraphicsInfo(); //slideDock.setSize(GUI.GScreenWidth / 2, GUI.GScreenHeight / 2); } }, new VueAction("Maximize") { public void act() { slideDock.setBounds(GUI.getMaximumWindowBounds(slideDock)); } } }; Widget.setMenuActions(this, mMenuActions); } */ /* private static class SingletonMap extends LWMap { private final LWMap srcMap; SingletonMap(LWMap srcMap, LWComponent singleChild) { super("SingletonMap"); this.srcMap = srcMap; super.children = java.util.Collections.singletonList(singleChild); } public tufts.vue.LWPathwayList getPathwayList() { return srcMap == null ? null : srcMap.getPathwayList(); } } */ }