/*FreeMind - A Program for creating and viewing Mindmaps *Copyright (C) 2000-2011 Joerg Mueller, Daniel Polansky, Christian Foltin, Dimitri Polivaev and others. * *See COPYING for Details * *This program is free software; you can redistribute it and/or *modify it under the terms of the GNU General Public License *as published by the Free Software Foundation; either version 2 *of the License, or (at your option) any later version. * *This program is distributed in the hope that it will be useful, *but WITHOUT ANY WARRANTY; without even the implied warranty of *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *GNU General Public License for more details. * *You should have received a copy of the GNU General Public License *along with this program; if not, write to the Free Software *Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package accessories.plugins; import java.awt.datatransfer.Transferable; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.Vector; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import freemind.common.OptionalDontShowMeAgainDialog; import freemind.controller.MenuItemEnabledListener; import freemind.controller.MindMapNodesSelection; import freemind.controller.actions.generated.instance.CompoundAction; import freemind.controller.actions.generated.instance.CutNodeAction; import freemind.controller.actions.generated.instance.DeleteNodeAction; import freemind.controller.actions.generated.instance.HookNodeAction; import freemind.controller.actions.generated.instance.MoveNodeXmlAction; import freemind.controller.actions.generated.instance.MoveNodesAction; import freemind.controller.actions.generated.instance.NewNodeAction; import freemind.controller.actions.generated.instance.NodeAction; import freemind.controller.actions.generated.instance.NodeListMember; import freemind.controller.actions.generated.instance.PasteNodeAction; import freemind.controller.actions.generated.instance.UndoPasteNodeAction; import freemind.controller.actions.generated.instance.XmlAction; import freemind.extensions.HookRegistration; import freemind.main.FreeMind; import freemind.main.Resources; import freemind.main.Tools; import freemind.main.Tools.MindMapNodePair; import freemind.modes.MindMap; import freemind.modes.MindMapNode; import freemind.modes.ModeController; import freemind.modes.ModeController.NodeSelectionListener; import freemind.modes.NodeAdapter; import freemind.modes.mindmapmode.MindMapController; import freemind.modes.mindmapmode.actions.NodeHookAction; import freemind.modes.mindmapmode.actions.xml.ActionFilter; import freemind.modes.mindmapmode.actions.xml.ActionPair; import freemind.modes.mindmapmode.hooks.MindMapNodeHookAdapter; import freemind.view.mindmapview.NodeView; /** * This is the "paste node as clone" action from the menu. * * @author foltin * @date 25.4.2011 * */ public class ClonePasteAction extends MindMapNodeHookAdapter { /** * */ public ClonePasteAction() { } public void invoke(MindMapNode pNode) { super.invoke(pNode); Vector mindMapNodes = getMindMapNodes(); logger.fine("Clones for nodes: " + Tools.listToString(mindMapNodes)); // now, construct the plugin for those nodes: for (Iterator itPastedNodes = mindMapNodes.iterator(); itPastedNodes .hasNext();) { MindMapNode copiedNode = (MindMapNode) itPastedNodes.next(); ClonePlugin clonePlugin = ClonePlugin.getHook(copiedNode); // first the clone master if (clonePlugin == null) { int showResult = new OptionalDontShowMeAgainDialog( getMindMapController().getFrame().getJFrame(), getMindMapController().getSelectedView(), "choose_clone_type", "clone_type_question", getMindMapController(), new OptionalDontShowMeAgainDialog.StandardPropertyHandler( getMindMapController().getController(), FreeMind.RESOURCES_COMPLETE_CLONING), OptionalDontShowMeAgainDialog.BOTH_OK_AND_CANCEL_OPTIONS_ARE_STORED) .show().getResult(); Properties properties = new Properties(); properties .setProperty( ClonePlugin.XML_STORAGE_CLONE_ITSELF, showResult == JOptionPane.OK_OPTION ? ClonePlugin.CLONE_ITSELF_TRUE : ClonePlugin.CLONE_ITSELF_FALSE); Vector selecteds = Tools.getVectorWithSingleElement(copiedNode); getMindMapController().addHook(copiedNode, selecteds, ClonePlugin.PLUGIN_LABEL, properties); } // finally, we construct a new one: Transferable copy = getMindMapController().copy(copiedNode, true); addNewClone(copiedNode, pNode, copy); } } public void addNewClone(MindMapNode originalNode, MindMapNode pDestinationNode, Transferable copy) { String originalNodeId = getMindMapController().getNodeID(originalNode); logger.fine("Original node " + originalNode + ", id " + originalNodeId); if (originalNode.isRoot()) { getMindMapController().getController().errorMessage( getMindMapController().getText( "clone_plugin_no_root_cloning")); return; } // insert clone: List listOfChilds = pDestinationNode.getChildren(); Vector listOfChildIds = new Vector(); for (Iterator it = listOfChilds.iterator(); it.hasNext();) { String nodeID = getMindMapController().getNodeID( (MindMapNode) it.next()); listOfChildIds.add(nodeID); logger.fine("Old child id:" + nodeID); } getMindMapController().paste(copy, pDestinationNode); } public Vector getMindMapNodes() { return getRegistration().getMindMapNodes(); } protected Registration getRegistration() { return (Registration) getPluginBaseClass(); } public interface ClonePropertiesObserver { void propertiesChanged(CloneProperties pCloneProperties); } public static class CloneProperties { boolean mCloneItself = false; private HashSet mObserverSet = new HashSet(); protected static java.util.logging.Logger logger = null; /** * */ public CloneProperties() { if (logger == null) { logger = freemind.main.Resources.getInstance().getLogger( this.getClass().getName()); } } public boolean isCloneItself() { return mCloneItself; } public void setCloneItself(boolean pCloneItself) { logger.finest("Setting mCloneItself to " + pCloneItself); boolean fire = false; if (pCloneItself != mCloneItself) { fire = true; } mCloneItself = pCloneItself; if (fire) { firePropertiesChanged(); } } public void registerObserver(ClonePropertiesObserver pObserver) { mObserverSet.add(pObserver); } public void deregisterObserver(ClonePropertiesObserver pObserver) { mObserverSet.remove(pObserver); } private void firePropertiesChanged() { for (Iterator it = mObserverSet.iterator(); it.hasNext();) { ClonePropertiesObserver observer = (ClonePropertiesObserver) it .next(); observer.propertiesChanged(this); } } } public static class Registration implements HookRegistration, MenuItemEnabledListener, ActionFilter, NodeSelectionListener { private static final String PLUGIN_NAME = "accessories/plugins/ClonePasteAction.properties"; private static ImageIcon sCloneIcon; private static ImageIcon sOriginalIcon; private static boolean sShowIcon = true; /** * Mapping of clone id (String) to a HashSet of {@link MindMapNode}s */ private HashMap mCloneIdsMap = new HashMap(); /** * This is the reverse of mCloneIdsMap: {@link MindMapNode} to cloneId. */ private HashMap mClonesMap = new HashMap(); /** * This is a storage cloneId to clone properties. */ private HashMap mClonePropertiesMap = new HashMap(); private final MindMapController controller; private final MindMap mMap; private final java.util.logging.Logger logger; private Vector mLastMarkedNodeViews = new Vector(); public Registration(ModeController controller, MindMap map) { this.controller = (MindMapController) controller; mMap = map; logger = controller.getFrame().getLogger(this.getClass().getName()); } public void register() { if (sCloneIcon == null) { sCloneIcon = new ImageIcon( controller.getResource("images/clone.png")); sOriginalIcon = new ImageIcon( controller.getResource("images/clone_original.png")); sShowIcon = Resources.getInstance().getBoolProperty( FreeMind.RESOURCES_DON_T_SHOW_CLONE_ICONS); } controller.getActionFactory().registerFilter(this); controller.registerNodeSelectionListener(this, false); } public void deRegister() { controller.deregisterNodeSelectionListener(this); controller.getActionFactory().deregisterFilter(this); } public boolean isEnabled(JMenuItem pItem, Action pAction) { if (controller == null) return false; String hookName = ((NodeHookAction) pAction).getHookName(); if (PLUGIN_NAME.equals(hookName)) { // only enabled, if nodes have been copied before. Vector mindMapNodes = getMindMapNodes(); // logger.warning("Nodes " + Tools.listToString(mindMapNodes)); return !mindMapNodes.isEmpty(); } List selecteds = controller.getSelecteds(); for (Iterator it = selecteds.iterator(); it.hasNext();) { MindMapNode node = (MindMapNode) it.next(); if (ClonePlugin.getHook(node) != null) { return true; } } return false; } public Vector getMindMapNodes() { Vector mindMapNodes = new Vector(); Transferable clipboardContents = controller.getClipboardContents(); if (clipboardContents != null) { try { List transferData = (List) clipboardContents .getTransferData(MindMapNodesSelection.copyNodeIdsFlavor); for (Iterator it = transferData.iterator(); it.hasNext();) { String nodeId = (String) it.next(); MindMapNode node = controller.getNodeFromID(nodeId); mindMapNodes.add(node); } } catch (Exception e) { // e.printStackTrace(); // freemind.main.Resources.getInstance().logException(e); } } return mindMapNodes; } public String generateNewCloneId(String pProposedID) { return Tools.generateID(pProposedID, mCloneIdsMap, "CLONE_"); } /** * @param pCloneId * @return true, if the pCloneId is new (not already registered) */ public boolean registerClone(String pCloneId, ClonePlugin pPlugin) { boolean vectorPresent = mCloneIdsMap.containsKey(pCloneId); HashSet v = getHashSetToCloneId(pCloneId); MindMapNode node = pPlugin.getNode(); for (Iterator it = v.iterator(); it.hasNext();) { MindMapNode otherCloneNode = (MindMapNode) it.next(); ClonePlugin otherClone = ClonePlugin.getHook(otherCloneNode); if (otherClone == null) { it.remove(); logger.warning("Found clone node " + controller.getNodeID(otherCloneNode) + " which isn't a clone any more."); continue; } // inform all others otherClone.addClone(node); // inform this clone about its brothers pPlugin.addClone(otherCloneNode); } v.add(node); mClonesMap.put(node, pCloneId); selectShadowNode(node, true, node); if (!mClonePropertiesMap.containsKey(pCloneId)) { mClonePropertiesMap.put(pCloneId, new CloneProperties()); } return !vectorPresent; } public void deregisterClone(String pCloneId, ClonePlugin pPlugin) { HashSet cloneSet = getHashSetToCloneId(pCloneId); MindMapNode node = pPlugin.getNode(); cloneSet.remove(node); mClonesMap.remove(node); // inform all others for (Iterator it = cloneSet.iterator(); it.hasNext();) { MindMapNode otherCloneNode = (MindMapNode) it.next(); ClonePlugin otherClone = ClonePlugin.getHook(otherCloneNode); if (otherClone == null) { it.remove(); logger.warning("Found clone node " + controller.getNodeID(otherCloneNode) + " which isn't a clone any more."); continue; } otherClone.removeClone(node); } if (cloneSet.isEmpty()) { // remove entire clone mCloneIdsMap.remove(cloneSet); mClonePropertiesMap.remove(pCloneId); } } public CloneProperties getCloneProperties(String pCloneId) { if (mClonePropertiesMap.containsKey(pCloneId)) { return (CloneProperties) mClonePropertiesMap.get(pCloneId); } throw new IllegalArgumentException( "Clone properties not found for " + pCloneId); } protected HashSet getHashSetToCloneId(String pCloneId) { HashSet v = null; if (!mCloneIdsMap.containsKey(pCloneId)) { v = new HashSet(); mCloneIdsMap.put(pCloneId, v); } else { v = (HashSet) mCloneIdsMap.get(pCloneId); } return v; } public ActionPair filterAction(ActionPair pair) { // shortcut for no clones for speed up. if (mCloneIdsMap.isEmpty()) { return pair; } XmlAction doAction = pair.getDoAction(); doAction = cloneAction(doAction); pair.setDoAction(doAction); return pair; } private XmlAction cloneAction(XmlAction doAction) { logger.fine("Found do action: " + doAction.getClass().getName()); if (doAction instanceof NodeAction) { NodeAction nodeAction = (NodeAction) doAction; MindMapNode node = controller.getNodeFromID(nodeAction .getNode()); // check for clone or original? doAction = cloneAction(nodeAction, node); } else { if (doAction instanceof CompoundAction) { CompoundAction compoundAction = (CompoundAction) doAction; List choiceList = compoundAction.getListChoiceList(); int index = 0; for (Iterator it = choiceList.iterator(); it.hasNext();) { XmlAction subAction = (XmlAction) it.next(); subAction = cloneAction(subAction); compoundAction.setAtChoice(index, subAction); index++; } } } return doAction; } private XmlAction cloneAction(NodeAction nodeAction, MindMapNode node) { List correspondingNodes = getCorrespondingNodes(nodeAction, node); if (correspondingNodes.isEmpty()) { return nodeAction; } // create new action: CompoundAction compound = new CompoundAction(); compound.addChoice(nodeAction); for (Iterator it = correspondingNodes.iterator(); it.hasNext();) { Tools.MindMapNodePair pair = (Tools.MindMapNodePair) it.next(); getNewCompoundAction(nodeAction, pair, compound); } return compound; } private void getNewCompoundAction(NodeAction nodeAction, Tools.MindMapNodePair correspondingNodePair, CompoundAction compound) { // deep copy: NodeAction copiedNodeAction = (NodeAction) Tools .deepCopy(nodeAction); // special cases: if (copiedNodeAction instanceof MoveNodesAction) { MoveNodesAction moveAction = (MoveNodesAction) copiedNodeAction; for (int i = 0; i < moveAction.getListNodeListMemberList() .size(); i++) { NodeListMember member = moveAction.getNodeListMember(i); changeNodeListMember(correspondingNodePair, moveAction, member); } } if (copiedNodeAction instanceof HookNodeAction) { HookNodeAction hookAction = (HookNodeAction) copiedNodeAction; for (int i = 0; i < hookAction.getListNodeListMemberList() .size(); i++) { NodeListMember member = hookAction.getNodeListMember(i); changeNodeListMember(correspondingNodePair, hookAction, member); } } if (copiedNodeAction instanceof NewNodeAction) { NewNodeAction newNodeAction = (NewNodeAction) copiedNodeAction; String newId = mMap.getLinkRegistry().generateUniqueID(null); newNodeAction.setNewId(newId); } copiedNodeAction.setNode(controller.getNodeID(correspondingNodePair .getCorresponding())); if (copiedNodeAction instanceof PasteNodeAction) { /* * difficult thing here: if something is pasted, the paste * action itself contains the node ids of the paste. The first * pasted action will get that node id. This should be the * corresponding node itself. This presumably corrects a bug * that the selection on move actions is changing. */ compound.addChoice(copiedNodeAction); } else { compound.addAtChoice(0, copiedNodeAction); } } public void changeNodeListMember( Tools.MindMapNodePair correspondingNodePair, NodeAction pAction, NodeListMember member) { NodeAdapter memberNode = controller.getNodeFromID(member.getNode()); List correspondingMoveNodes = getCorrespondingNodes(pAction, memberNode); if (!correspondingMoveNodes.isEmpty()) { // search for this clone: for (Iterator it = correspondingMoveNodes.iterator(); it .hasNext();) { Tools.MindMapNodePair pair = (Tools.MindMapNodePair) it .next(); if (pair.getCloneNode() == correspondingNodePair .getCloneNode()) { // found: member.setNode(controller.getNodeID(pair .getCorresponding())); break; } } } } /** * Method takes into account, that some actions are different. * * @param nodeAction * @param node * @return */ public List getCorrespondingNodes(NodeAction nodeAction, MindMapNode node) { boolean startWithParent = false; // Behavior for complete cloning. if (mClonesMap.containsKey(node)) { String cloneId = (String) mClonesMap.get(node); if (getCloneProperties(cloneId).isCloneItself()) { // Behavior for complete cloning if (nodeAction instanceof MoveNodesAction || nodeAction instanceof MoveNodeXmlAction || nodeAction instanceof DeleteNodeAction || nodeAction instanceof CutNodeAction) { // ok, there is an action for a clone itself. be // careful: // clone only, if parents are clones: startWithParent = true; } else if (nodeAction instanceof PasteNodeAction) { PasteNodeAction pna = (PasteNodeAction) nodeAction; if (pna.getAsSibling()) { // sibling means, that the paste goes below the // clone. // skip. startWithParent = true; } else { // here, the action changes the children, thus, they // are // subject to cloning. } } else if (nodeAction instanceof UndoPasteNodeAction) { UndoPasteNodeAction pna = (UndoPasteNodeAction) nodeAction; if (pna.getAsSibling()) { // sibling means, that the paste goes below the // clone. // skip. startWithParent = true; } else { // here, the action changes the children, thus, they // are // subject to cloning. } } } else { // Behavior for children cloning only /* * new node action belongs to the children, so clone it, * even, when node is the clone itself. */ if (nodeAction instanceof NewNodeAction) { // here, the action changes the children, thus, they are // subject to cloning. } else if (nodeAction instanceof PasteNodeAction) { PasteNodeAction pna = (PasteNodeAction) nodeAction; if (pna.getAsSibling()) { // sibling means, that the paste goes below the // clone. // skip. startWithParent = true; } else { // here, the action changes the children, thus, they // are // subject to cloning. } } else if (nodeAction instanceof UndoPasteNodeAction) { UndoPasteNodeAction pna = (UndoPasteNodeAction) nodeAction; if (pna.getAsSibling()) { // sibling means, that the paste goes below the // clone. // skip. startWithParent = true; } else { // here, the action changes the children, thus, they // are // subject to cloning. } } else { // ok, there is an action for a clone itself. be // careful: // clone only, if parents are clones: startWithParent = true; } } } List/* MindMapNodePair */correspondingNodes = getCorrespondingNodes( node, startWithParent); return correspondingNodes; } /** * This is the main method here. It returns to a given node its cloned * nodes on the other side. * * @param pNode * is checked to be son of one of the clones/original. * @param pStartWithParent * Sometimes, it is relevant, if only one of the parents is a * clone, eg. for all actions, that affect the clone itself, * thus not need to be cloned, but perhaps the clone is * itself a node inside of another clone! * @return a list of {@link MindMapNodePair}s where the first is the * corresponding node and the second is the clone. If the return * value is empty, the node isn't son of any. */ public List getCorrespondingNodes(MindMapNode pNode, boolean pStartWithParent) { // in case, no clones are present, this method returns very fast. if (mClonesMap.isEmpty()) { return Collections.EMPTY_LIST; } MindMapNode clone; { MindMapNode child; // code doubling to speed up. First check for a clone on the way // to root. if (pStartWithParent) { child = pNode.getParentNode(); } else { child = pNode; } while (!mClonesMap.containsKey(child)) { if (child.isRoot()) { // nothing found! return Collections.EMPTY_LIST; } child = child.getParentNode(); } clone = child; } MindMapNode child; // now, there is a clone on the way. Collect the indices. Vector indexVector = new Vector(); if (pStartWithParent) { addNodePosition(indexVector, pNode); child = pNode.getParentNode(); } else { child = pNode; } while (clone != child) { addNodePosition(indexVector, child); child = child.getParentNode(); } Vector returnValue = new Vector(); MindMapNode originalNode = child; HashSet targets = (HashSet) mCloneIdsMap.get(mClonesMap.get(child)); CloneLoop: for (Iterator itClone = targets.iterator(); itClone .hasNext();) { MindMapNode cloneNode = (MindMapNode) itClone.next(); MindMapNode target = cloneNode; if (cloneNode == originalNode) continue; for (int i = indexVector.size() - 1; i >= 0; --i) { int index = ((Integer) indexVector.get(i)).intValue(); if (target.getChildCount() <= index) { logger.warning("Index " + index + " in other tree not found from " + printNodeIds(targets) + " originating from " + printNodeId(cloneNode) + " start at parent " + pStartWithParent); // with crossed fingers. continue CloneLoop; } target = (MindMapNode) target.getChildAt(index); } // logger.fine("Found corresponding node " + printNodeId(target) // + " on clone " + printNodeId(cloneNode)); returnValue.add(new Tools.MindMapNodePair(target, cloneNode)); } return returnValue; } private void addNodePosition(Vector indexVector, MindMapNode child) { indexVector.add(new Integer(child.getParentNode().getChildPosition( child))); } /** * @param pCloneNode * @return */ private String printNodeId(MindMapNode pCloneNode) { try { return controller.getNodeID(pCloneNode) + ": '" + (pCloneNode.getShortText(controller)) + "'"; } catch (Exception e) { return "NOT FOUND: '" + pCloneNode + "'"; } } /** * @param pClones * @return */ private String printNodeIds(HashSet pClones) { Vector strings = new Vector(); for (Iterator it = pClones.iterator(); it.hasNext();) { MindMapNode pluginNode = (MindMapNode) it.next(); strings.add(printNodeId(pluginNode)); } return Tools.listToString(strings); } /** * Is sent when a node is selected. */ public void onFocusNode(NodeView node) { markShadowNode(node, true); } /** * Is sent when a node is deselected. */ public void onLostFocusNode(NodeView node) { markShadowNode(node, false); } public void markShadowNode(NodeView pNode, boolean pEnableShadow) { // at startup, the node is null. if (pNode == null || pNode.getModel() == null) { return; } if (!pEnableShadow) { if (!mLastMarkedNodeViews.isEmpty()) { for (Iterator it = mLastMarkedNodeViews.iterator(); it .hasNext();) { MindMapNode node = (MindMapNode) it.next(); if (mClonesMap.containsKey(node)) { setIcon(node, sOriginalIcon); } else { setIcon(node, null); } } mLastMarkedNodeViews.clear(); } } else { markShadowNode(pNode.getModel(), pEnableShadow); } } public void markShadowNode(MindMapNode model, boolean pEnableShadow) { mLastMarkedNodeViews.clear(); try { List/* pair of MindMapNodePair */shadowNodes = getCorrespondingNodes( model, false); for (Iterator it = shadowNodes.iterator(); it.hasNext();) { Tools.MindMapNodePair shadowNode = (Tools.MindMapNodePair) it .next(); MindMapNode correspondingNode = shadowNode .getCorresponding(); mLastMarkedNodeViews.add(correspondingNode); selectShadowNode(correspondingNode, pEnableShadow, shadowNode.getCloneNode()); } } catch (IllegalArgumentException e) { freemind.main.Resources.getInstance().logException(e); } } private void selectShadowNode(MindMapNode node, boolean pEnableShadow, MindMapNode pCloneNode) { if (!sShowIcon) { return; } while (node != null) { ImageIcon i = pEnableShadow ? sCloneIcon : null; if (node == pCloneNode) { i = sOriginalIcon; } setIcon(node, i); if (node == pCloneNode) break; node = node.getParentNode(); // comment this out to get a complete marked path to the root of // the // clones. break; } } public void setIcon(MindMapNode node, ImageIcon i) { node.setStateIcon(ClonePlugin.PLUGIN_LABEL, i); controller.nodeRefresh(node); } /* * (non-Javadoc) * * @see * freemind.modes.ModeController.NodeSelectionListener#onSelectionChange * (freemind.modes.MindMapNode, boolean) */ public void onSelectionChange(NodeView pNode, boolean pIsSelected) { } public void onUpdateNodeHook(MindMapNode pNode) { } public void onSaveNode(MindMapNode pNode) { } } }