/* * 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 java.util.*; import java.awt.event.KeyEvent; import javax.swing.*; import edu.tufts.vue.ontology.ui.OntologySelectionEvent; import tufts.vue.NodeTool.NodeModeTool; import java.awt.geom.Point2D; //import tufts.vue.beans.VueBeanState; /** * VueTool for creating links. Provides methods for creating default new links * based on the current state of the tools, as well as the handling drag-create * of new links. */ public class LinkTool extends VueTool implements VueConstants//, LWEditor { /** link tool contextual tool panel **/ // private static LinkToolPanel sLinkContextualPanel; private static LinkTool singleton = null; public LinkTool() { super(); //creationLink.setStrokeColor(java.awt.Color.blue); if (singleton != null) new Throwable("Warning: mulitple instances of " + this).printStackTrace(); singleton = this; VueToolUtils.setToolProperties(this,"linkTool"); } /** return the singleton instance of this class */ public static LinkTool getTool() { if (singleton == null) { // new Throwable("Warning: LinkTool.getTool: class not initialized by VUE").printStackTrace(); new LinkTool(); } return singleton; } /** @return an array of actions, with icon set, that will set the shape of selected * LinkTools */ public Action[] getSetterActions() { Action[] actions = new Action[getSubToolIDs().size()]; Enumeration e = getSubToolIDs().elements(); int i = 0; while (e.hasMoreElements()) { String id = (String) e.nextElement(); LinkTool.SubTool nt = (LinkTool.SubTool) getSubTool(id); actions[i++] = nt.getSetterAction(); } return actions; } public JPanel getContextualPanel() { return null;//getLinkToolPanel(); } /** @return LWLink.class */ @Override public Class getSelectionType() { return LWLink.class; } private static final Object LOCK = new Object(); /* static LinkToolPanel getLinkToolPanel() { synchronized (LOCK) { if (sLinkContextualPanel == null) sLinkContextualPanel = new LinkToolPanel(); } return sLinkContextualPanel; } */ /* final public Object getPropertyKey() { return LWKey.LinkShape; } public Object produceValue() { return new Integer(getActiveSubTool().getCurveCount()); } /** LWPropertyProducer impl: load the currently selected link tool to the one with given curve count public void displayValue(Object curveValue) { // Find the sub-tool with the matching curve-count, then load it's button icon images // into the displayed selection icon if (curveValue == null) return; Enumeration e = getSubToolIDs().elements(); int curveCount = ((Integer)curveValue).intValue(); while (e.hasMoreElements()) { String id = (String) e.nextElement(); SubTool subtool = (SubTool) getSubTool(id); if (subtool.getCurveCount() == curveCount) { ((PaletteButton)mLinkedButton).setPropertiesFromItem(subtool.mLinkedButton); // call super.setSelectedSubTool to avoid firing the setters // as we're only LOADING the value here. super.setSelectedSubTool(subtool); break; } } } */ public void setSelectedSubTool(VueTool tool) { super.setSelectedSubTool(tool); if (VUE.getSelection().size() > 0) { SubTool subTool = (SubTool) tool; subTool.getSetterAction().fire(this); } } public SubTool getActiveSubTool() { return (SubTool) getSelectedSubTool(); } public boolean supportsSelection() { return true; } static void setMapIndicationIfOverValidTarget(LWComponent linkSource, LWLink link, MapMouseEvent e) { final MapViewer viewer = e.getViewer(); final LWComponent over = pickLinkTarget(link, e); if (DEBUG.CONTAINMENT || DEBUG.PICK || DEBUG.LINK) System.out.println("LINK-TARGET-POTENTIAL: " + over + "; src=" + linkSource + "; link=" + link); if (over != null && isValidLinkTarget(link, linkSource, over)) { viewer.setIndicated(over); } else { final LWComponent currentIndication = viewer.getIndication(); if (currentIndication != null && currentIndication != over) viewer.clearIndicated(); } } private static LWComponent pickLinkTarget(LWLink link, MapMouseEvent e) { PickContext pc = e.getViewer().getPickContext(e.getMapX(), e.getMapY()); pc.excluded = link; // this will override default focal exclusion: check manually below final LWComponent hit = LWTraversal.PointPick.pick(pc); if (hit == e.getViewer().getFocal()) return null; else return hit; } private static void reject(LWComponent target, String reason) { System.out.println("LinkTool; rejected: " + reason + "; " + target); } /** * Make sure we don't create any links back on themselves. * * @param linkSource -- LWComponent at far (anchor) end of * the link we're trying to find another endpoint for -- can * be null, meaning unattached. * @param linkTarget -- LWComponent at dragged end * the link we're looking for an endpoint with. * @return true if linkTarget is a valid link endpoint given our other end anchored at linkSource */ static boolean isValidLinkTarget(LWLink link, LWComponent linkSource, LWComponent linkTarget) { if (linkTarget == linkSource && linkSource != null) { if (DEBUG.LINK) reject(linkTarget, "source == target"); return false; } // TODO: allow loop-back link if it's a CURVED link... // don't allow links between parents & children if (linkSource != null) { if (linkTarget.getParent() == linkSource || linkSource.getParent() == linkTarget) { if (DEBUG.LINK) reject(linkTarget, "parent-child link"); return false; } if (linkTarget != null) { if (!linkSource.canLinkTo(linkTarget)) { if (DEBUG.LINK) reject(linkTarget, "source denies target; src=" + linkSource); return false; } } } if (link != null && linkTarget == link.getParent()) { // if a link is inside something, don't link to it (?) if (DEBUG.LINK) reject(linkTarget, "target is parent of the new link"); return false; } if (link != null && linkTarget instanceof LWLink && linkTarget.isConnectedTo(link)) { // Don't permit a link to link back to a link that's connected to it. if (DEBUG.LINK) reject(linkTarget, "this link already connected to target"); return false; } // New code, tho we don't actually need these constraints: // if (linkTarget instanceof LWLink) { // if (DEBUG.LINK) outln("target is link: " + linkTarget); // if (linkTarget.isConnectedTo(linkSource)) { // if (DEBUG.LINK) reject(linkTarget, "target link-to-link loop"); // return false; // } else if (link != null && linkTarget.isConnectedTo(link)) { // if (DEBUG.LINK) reject(linkTarget, "this link already connected to target"); // return false; // } // } // if (linkSource instanceof LWLink) { // if (linkSource.isConnectedTo(linkTarget)) { // if (DEBUG.LINK) reject(linkTarget, "source link-to-link loop; src=" + linkSource); // return false; // }// else if (link != null // } return true; // The old code: // boolean ok = true; // if (linkTarget instanceof LWLink) { // LWLink target = (LWLink) linkTarget; // ok &= (target.getHead() != linkSource && // target.getTail() != linkSource); // } // if (linkSource instanceof LWLink) { // LWLink source = (LWLink) linkSource; // ok &= (source.getHead() != linkTarget && // source.getTail() != linkTarget); // } // return ok; } public void drawSelector(DrawContext dc, java.awt.Rectangle r) { //g.setXORMode(java.awt.Color.blue); dc.g.setColor(java.awt.Color.blue); super.drawSelector(dc, r); } /* private void makeLink(MapMouseEvent e, LWComponent pLinkSource, LWComponent pLinkDest, boolean pMakeConnection) { LWLink existingLink = null; if (pLinkDest != null) existingLink = pLinkDest.getLinkTo(pLinkSource); if (false && existingLink != null) { // There's already a link tween these two -- increment the weight // [ WE NOW ALLOW MULTIPLE LINKS BETWEEN NODES ] existingLink.incrementWeight(); } else { // TODO: don't create new node at end of new link inside // parent of source node (e.g., content view/traditional node) // unless mouse is over that node! (E.g., should be able to // drag link out from a node that is a child) LWContainer commonParent = e.getMap(); if (pLinkDest == null) commonParent = pLinkSource.getParent(); else if (pLinkSource.getParent() == pLinkDest.getParent() && pLinkSource.getParent() != commonParent) { // todo: if parents different, add to the upper most parent commonParent = pLinkSource.getParent(); } boolean createdNode = false; if (pLinkDest == null && pMakeConnection) { pLinkDest = NodeModeTool.createNewNode(); pLinkDest.setCenterAt(e.getMapPoint()); commonParent.addChild(pLinkDest); createdNode = true; } LWLink link; if (pMakeConnection) { link = new LWLink(pLinkSource, pLinkDest); } else { link = new LWLink(pLinkSource, null); link.setTailPoint(e.getMapPoint()); // set to drop location } commonParent.addChild(link); // We ensure a paint sequence here because a link to a link // is currently drawn to it's center, which might paint over // a label. if (pLinkSource instanceof LWLink) commonParent.ensurePaintSequence(link, pLinkSource); if (pLinkDest instanceof LWLink) commonParent.ensurePaintSequence(link, pLinkDest); //VUE.getSelection().setTo(link); if (pMakeConnection) e.getViewer().activateLabelEdit(createdNode ? pLinkDest : link); } } */ static class ComboModeTool extends LinkModeTool { public ComboModeTool() { super(); setComboMode(true); // Mac overrides CONTROL-MOUSE to look like right-click (context menu popup) so we can't // use CTRL wih mouse drag on a mac. setActiveWhileDownKeyCode(KeyEvent.VK_ALT); } } public static class OntologyLinkModeTool extends LinkModeTool implements edu.tufts.vue.ontology.ui.OntologySelectionListener { // private final LWComponent invisibleLinkEndpoint = new LWComponent(); // private LWLink creationLink = new LWLink(invisibleLinkEndpoint); public OntologyLinkModeTool() { edu.tufts.vue.ontology.ui.OntologyBrowser.getBrowser().addOntologySelectionListener(this); //creationLink.setID("<creationLink>"); // can't use label or it will draw one //invisibleLinkEndpoint.addLinkRef(creationLink); creationLink=null; invisibleLinkEndpoint.setSize(0,0); } public boolean handleMousePressed(MapMouseEvent e) { if (creationLink == null) { VueUtil.alert(VueResources.getString("ontologyLinkError.message"), VueResources.getString("ontologyLinkError.title")); return true; } else return false; } @Override public boolean handleMouseReleased(MapMouseEvent e) { //System.out.println(this + " " + e + " linkSource=" + linkSource); if (linkSource == null) return false; //System.out.println("dx,dy=" + e.getDeltaPressX() + "," + e.getDeltaPressY()); if (Math.abs(e.getDeltaPressX()) > 10 || Math.abs(e.getDeltaPressY()) > 10) // todo: config min dragout distance { //repaintMapRegionAdjusted(creationLink.getBounds()); LWComponent linkDest = e.getViewer().getIndication(); if (linkDest != linkSource) makeLink(e, linkSource, linkDest, !e.isShiftDown(),false); } this.linkSource = null; return true; } private void makeLink(MapMouseEvent e, LWComponent pLinkSource, LWComponent pLinkDest, boolean pMakeConnection, boolean comboMode) { int existingLinks = 0; int existingCurvedLinks = 0; if (pLinkDest != null) { existingLinks = pLinkDest.countLinksTo(pLinkSource); existingCurvedLinks = pLinkDest.countCurvedLinksTo(pLinkSource); } final int existingStraightLinks = existingLinks - existingCurvedLinks; // TODO: don't create new node at end of new link inside // parent of source node (e.g., content view/traditional node) // unless mouse is over that node! (E.g., should be able to // drag link out from a node that is a child) LWContainer commonParent = e.getMap(); if (pLinkDest == null) commonParent = pLinkSource.getParent(); else if (pLinkSource.getParent() == pLinkDest.getParent() && pLinkSource.getParent() != commonParent) { // todo: if parents different, add to the upper most parent commonParent = pLinkSource.getParent(); } boolean createdNode = false; if (pLinkDest == null && (pMakeConnection && comboMode)) { pLinkDest = NodeModeTool.createNewNode(); pLinkDest.setCenterAt(e.getMapPoint()); commonParent.addChild(pLinkDest); createdNode = true; } LWLink link; if (pMakeConnection && (comboMode || pLinkDest!=null)) { //link = new LWLink(pLinkSource, pLinkDest); link = (LWLink)creationLink.duplicate(); link.setHead(pLinkSource); link.setTail(pLinkDest); if (existingStraightLinks > 0) link.setControlCount(1); } else { link = (LWLink)creationLink.duplicate();//new LWLink(pLinkSource, null); link.setTailPoint(e.getMapPoint()); // set to drop location } // EditorManager.targetAndApplyCurrentProperties(link); commonParent.addChild(link); // We ensure a paint sequence here because a link to a link // is currently drawn to it's center, which might paint over // a label. if (pLinkSource instanceof LWLink) commonParent.ensurePaintSequence(link, pLinkSource); if (pLinkDest instanceof LWLink) commonParent.ensurePaintSequence(link, pLinkDest); VUE.getSelection().setTo(link); if (pMakeConnection && comboMode) e.getViewer().activateLabelEdit(createdNode ? pLinkDest : link); } @Override public boolean handleComponentPressed(MapMouseEvent e) { //System.out.println(this + " handleMousePressed " + e); LWComponent hit = e.getPicked(); // TODO: handle LWGroup picking //if (hit instanceof LWGroup) //hit = ((LWGroup)hit).findDeepestChildAt(e.getMapPoint()); if (hit != null && hit.canLinkTo(null)) { linkSource = hit; // todo: pick up current default stroke color & stroke width // and apply to creationLink creationLink.setTemporaryEndPoint1(linkSource); // EditorManager.applyCurrentProperties(creationLink); // never let drawn creator link get less than 1 pixel wide on-screen float minStrokeWidth = (float) (1 / e.getViewer().getZoomFactor()); if (creationLink.getStrokeWidth() < minStrokeWidth) creationLink.setStrokeWidth(minStrokeWidth); invisibleLinkEndpoint.setLocation(e.getMapPoint()); e.setDragRequest(invisibleLinkEndpoint); // using a LINK as the dragComponent is a mess because geting the // "location" of a link isn't well defined if any end is tied // down, and so computing the relative movement of the link // doesn't work -- thus we just use this invisible endpoint // to move the link around. return true; } return false; } edu.tufts.vue.ontology.ui.TypeList list = null; public void ontologySelected(OntologySelectionEvent e) { edu.tufts.vue.ontology.ui.TypeList l = e.getSelection(); if (list != l) creationLink=null; list = l; LWComponent c = l.getSelectedComponent(); if (c != null) { if (c instanceof LWLink) { creationLink = (LWLink)c; // creationLink.setID("<creationLink>"); // can't use label or it will draw one invisibleLinkEndpoint.addLinkRef(creationLink); invisibleLinkEndpoint.setSize(0,0); creationLink.setTail(invisibleLinkEndpoint);// } } } } static class LinkModeTool extends VueTool { private boolean comboMode = false; protected LWComponent linkSource; // for starting a link protected final LWComponent invisibleLinkEndpoint = new LWComponent() { @Override public void setMapLocation(double x, double y) { super.takeLocation((float)x, (float)y); creationLink.notifyEndpointMoved(null, this); } }; protected LWLink creationLink = new LWLink(invisibleLinkEndpoint); public LinkModeTool() { super(); invisibleLinkEndpoint.takeSize(0,0); invisibleLinkEndpoint.setID("<invisibleLinkEndpoint>"); // can't use label or it will have a size > 0, offsetting the creationLink endpoint creationLink.setArrowState(LWLink.ARROW_TAIL); creationLink.setID("<creationLink>"); // can't use label or it will draw one as it's being dragged around // VueToolUtils.setToolProperties(this,"linkModeTool"); // Mac overrides CONTROL-MOUSE to look like right-click (context menu popup) so we can't // use CTRL wih mouse drag on a mac. setActiveWhileDownKeyCode(0); //setActiveWhileDownKeyCode(VueUtil.isMacPlatform() ? KeyEvent.VK_ALT : KeyEvent.VK_CONTROL); } /** @return LWLink.class */ @Override public Class getSelectionType() { return LWLink.class; } @Override public void handleDragAbort() { this.linkSource = null; } @Override public void handlePostDraw(DrawContext dc, MapViewer viewer) { if (linkSource != null) creationLink.draw(dc); } public void setComboMode(boolean comboMode) { this.comboMode = comboMode; } @Override public boolean handleComponentPressed(MapMouseEvent e) { //System.out.println(this + " handleMousePressed " + e); LWComponent hit = e.getPicked(); // TODO: handle LWGroup picking //if (hit instanceof LWGroup) //hit = ((LWGroup)hit).findDeepestChildAt(e.getMapPoint()); if (hit != null && hit.canLinkTo(null)) { linkSource = hit; // todo: pick up current default stroke color & stroke width // and apply to creationLink creationLink.setParent(linkSource.getParent()); // needed for new relative-to-parent link code //invisibleLinkEndpoint.setParent(linkSource.getParent()); // needed for new relative-to-parent link code creationLink.setTemporaryEndPoint1(linkSource); EditorManager.applyCurrentProperties(creationLink); // don't target until / unless link actually created // never let drawn creator link get less than 1 pixel wide on-screen float minStrokeWidth = (float) (1 / e.getViewer().getZoomFactor()); if (creationLink.getStrokeWidth() < minStrokeWidth) creationLink.setStrokeWidth(minStrokeWidth); invisibleLinkEndpoint.setLocation(e.getMapPoint()); creationLink.notifyEndpointMoved(null, invisibleLinkEndpoint); e.setDragRequest(invisibleLinkEndpoint); // using a LINK as the dragComponent is a mess because geting the // "location" of a link isn't well defined if any end is tied // down, and so computing the relative movement of the link // doesn't work -- thus we just use this invisible endpoint // to move the link around. return true; } return false; } @Override public boolean handleMouseDragged(MapMouseEvent e) { if (linkSource == null) return false; setMapIndicationIfOverValidTarget(linkSource, null, e); //------------------------------------------------------- // we're dragging a new link looking for an // allowable endpoint //------------------------------------------------------- return true; } @Override public boolean handleMouseReleased(MapMouseEvent e) { //System.out.println(this + " " + e + " linkSource=" + linkSource); if (linkSource == null) return false; //System.out.println("dx,dy=" + e.getDeltaPressX() + "," + e.getDeltaPressY()); if (Math.abs(e.getDeltaPressX()) > 10 || Math.abs(e.getDeltaPressY()) > 10) // todo: config min dragout distance { //repaintMapRegionAdjusted(creationLink.getBounds()); LWComponent linkDest = e.getViewer().getIndication(); if (linkDest != linkSource) makeLink(e, linkSource, linkDest, !e.isShiftDown(),comboMode); } this.linkSource = null; return true; } @Override public void drawSelector(DrawContext dc, java.awt.Rectangle r) { //g.setXORMode(java.awt.Color.blue); dc.g.setColor(java.awt.Color.blue); super.drawSelector(dc, r); } private void makeLink(MapMouseEvent e, LWComponent pLinkSource, LWComponent pLinkDest, boolean pMakeConnection, boolean comboMode) { int existingLinks = 0; int existingCurvedLinks = 0; if (pLinkDest != null) { existingLinks = pLinkDest.countLinksTo(pLinkSource); existingCurvedLinks = pLinkDest.countCurvedLinksTo(pLinkSource); } final int existingStraightLinks = existingLinks - existingCurvedLinks; // TODO: don't create new node at end of new link inside // parent of source node (e.g., content view/traditional node) // unless mouse is over that node! (E.g., should be able to // drag link out from a node that is a child) LWContainer commonParent = e.getMap(); if (pLinkDest == null) commonParent = pLinkSource.getParent(); else if (pLinkSource.getParent() == pLinkDest.getParent() && pLinkSource.getParent() != commonParent) { // todo: if parents different, add to the upper most parent commonParent = pLinkSource.getParent(); } boolean createdNode = false; if (pLinkDest == null && (pMakeConnection && comboMode)) { pLinkDest = NodeModeTool.createNewNode(); pLinkDest.setCenterAt(e.getMapPoint()); commonParent.addChild(pLinkDest); createdNode = true; } final LWLink link; if (pMakeConnection && (comboMode || pLinkDest!=null)) { link = new LWLink(pLinkSource, pLinkDest); if (existingStraightLinks > 0) link.setControlCount(1); } else { link = new LWLink(pLinkSource, null); link.setTailPoint(e.getMapPoint()); // set to drop location } EditorManager.targetAndApplyCurrentProperties(link); commonParent.addChild(link); // We ensure a paint sequence here because a link to a link // is currently drawn to it's center, which might paint over // a label. if (pLinkSource instanceof LWLink) commonParent.ensurePaintSequence(link, pLinkSource); if (pLinkDest instanceof LWLink) commonParent.ensurePaintSequence(link, pLinkDest); VUE.getSelection().setTo(link); if (pMakeConnection) e.getViewer().activateLabelEdit(createdNode ? pLinkDest : link); } } /** * VueTool class for each of the specifc link styles (straight, curved, etc). Knows how to generate * an action for setting the shape. */ public static class SubTool extends VueSimpleTool { private int curveCount = -1; private VueAction setterAction = null; public SubTool() {} public void setID(String pID) { super.setID(pID); try { curveCount = Integer.parseInt(getAttribute("curves")); } catch (Exception e) { e.printStackTrace(); } } public int getCurveCount() { return curveCount; } /** @return an action that will set the style of selected * LWLinks to the current link style for this SubTool */ public VueAction getSetterAction() { if (setterAction == null) { setterAction = new Actions.LWCAction(getToolName(), getIcon()) { void act(LWLink c) { c.setControlCount(curveCount); } }; setterAction.putValue("property.value", new Integer(curveCount)); // this may be handy setterAction.putValue(Action.SMALL_ICON, this.getRawIcon()); // key is from: MenuButton.ValueKey } return setterAction; } } }