diff --git a/src/multidom/MultiDOM.java b/src/multidom/MultiDOM.java index 8c24163..0a0805d 100644 --- a/src/multidom/MultiDOM.java +++ b/src/multidom/MultiDOM.java @@ -148,6 +148,7 @@ public SingleDOM loadDocument(String name) throws Exception { SingleDOM doc = newDocument(); doc.setName(name); doc.loadDocument(); // can throw Exception! + setActiveDocument(doc); return doc; } diff --git a/src/pooledit/Main.java b/src/pooledit/Main.java index 2011e05..5a1f96c 100644 --- a/src/pooledit/Main.java +++ b/src/pooledit/Main.java @@ -38,6 +38,7 @@ import attributetable.AttributeTable; import attributetable.AttributeTableModel; import java.awt.BorderLayout; +import java.awt.FlowLayout; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -71,6 +72,8 @@ import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.JFileChooser; +import javax.swing.JPanel; +import javax.swing.KeyStroke; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.TreeSelectionEvent; @@ -288,6 +291,7 @@ public void valueChanged(TreeSelectionEvent e) { @Override public void stateChanged(ChangeEvent e) { objecttree.setActivePath(multidom.getActivePath()); + model.setActivePath(multidom.getActivePath()); } }); @@ -517,6 +521,23 @@ public void windowClosing(DockingWindow window) throws OperationAbortedException // Add a mouse button listener that closes a window when it's // clicked with the middle mouse button. rootWindow.addTabMouseButtonListener(DockingWindowActionMouseButtonListener.MIDDLE_BUTTON_CLOSE_LISTENER); + + rootWindow.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control Z"), "undo"); + rootWindow.getActionMap().put("undo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + multidom.getActiveDocument().getTreeModel().undoAction(); + } + }); + + rootWindow.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control Y"), "redo"); + rootWindow.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control shift Z"), "redo"); + rootWindow.getActionMap().put("redo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + multidom.getActiveDocument().getTreeModel().redoAction(); + } + }); } /** @@ -614,8 +635,11 @@ private void showFrame() { * Creates the frame tool bar. * @return the frame tool bar */ - private JToolBar createToolBar() { - JToolBar toolBar = new JToolBar(); + private JPanel createToolBar() { + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT)); + + JToolBar toolBarObjects = new JToolBar(); TransferHandler hnd = new TransferHandler("XML"); MouseListener listener = new MouseAdapter() { @@ -630,10 +654,10 @@ public void mousePressed(MouseEvent evt) { JLabel lbl = new DragLabel(Definitions.OBJECTS[i], libdoc); lbl.setTransferHandler(hnd); lbl.addMouseListener(listener); - Border bdr = BorderFactory.createRaisedBevelBorder(); + Border bdr = BorderFactory.createRaisedBevelBorder(); lbl.setBorder(bdr); - toolBar.add(lbl); - toolBar.addSeparator(new Dimension(2, 1)); + toolBarObjects.add(lbl); + toolBarObjects.addSeparator(new Dimension(2, 1)); } /* @@ -651,7 +675,35 @@ public DockingWindowDragger getDragger(MouseEvent mouseEvent) { } }); */ - return toolBar; + + JToolBar toolBarTools = new JToolBar(); + + JButton btnUndo = new JButton("Undo"); + btnUndo.setToolTipText("Undo"); + btnUndo.setBorder(BorderFactory.createRaisedBevelBorder()); + btnUndo.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + multidom.getActiveDocument().getTreeModel().undoAction(); + } + }); + toolBarTools.add(btnUndo); + + JButton btnRedo = new JButton("Redo"); + btnRedo.setToolTipText("Redo"); + btnRedo.setBorder(BorderFactory.createRaisedBevelBorder()); + btnRedo.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + multidom.getActiveDocument().getTreeModel().redoAction(); + } + }); + toolBarTools.add(btnRedo); + + panel.add(toolBarObjects); + panel.add(toolBarTools); + + return panel; } /** @@ -1435,7 +1487,7 @@ public void actionPerformed(ActionEvent e) { "3 of the License, or (at your option) any later version.", " ", "Authors:", - "Matti Öhman (matti.ohman@aalto.fi)", + "Matti Ohman (matti.ohman@aalto.fi)", "Jouko Kalmari"}, "About PoolEdit", JOptionPane.INFORMATION_MESSAGE, Icons.POOLEDIT_LOGO); } diff --git a/src/treemodel/XMLTreeAction.java b/src/treemodel/XMLTreeAction.java new file mode 100644 index 0000000..7ec60c1 --- /dev/null +++ b/src/treemodel/XMLTreeAction.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 Automation technology laboratory, + * Helsinki University of Technology + * + * Visit automation.tkk.fi for information about the automation + * technology laboratory. + * + * 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 3 + * 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 treemodel; + +import javax.swing.tree.TreePath; +import org.w3c.dom.Element; + +/** + * + * @author Alan Valmorbida + */ +public class XMLTreeAction { + public int type; + public Element elem; + public TreePath parentPath; + public Element nextSibling; + public String name; + public String newValue; + public String oldValue; + + public static final int actionRemove = 0; + public static final int actionInsert = 1; + public static final int actionChange = 2; + + + public XMLTreeAction(int type, Element elem, TreePath parentPath, Element nextSibling, String name, String newValue, String oldValue) { + this.type = type; + this.elem = elem; + this.parentPath = parentPath; + this.nextSibling = nextSibling; + this.name = name; + this.newValue = newValue; + this.oldValue = oldValue; + } +} diff --git a/src/treemodel/XMLTreeModel.java b/src/treemodel/XMLTreeModel.java index d85fec7..d128f38 100644 --- a/src/treemodel/XMLTreeModel.java +++ b/src/treemodel/XMLTreeModel.java @@ -33,6 +33,7 @@ import org.w3c.dom.events.EventTarget; import org.w3c.dom.events.MutationEvent; import java.util.HashMap; +import java.util.LinkedList; import java.util.Map; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; @@ -60,6 +61,14 @@ public class XMLTreeModel implements TreeModel, EventListener { private final Map childMap; + private LinkedList undoActions = new LinkedList<>(); + private LinkedList redoActions = new LinkedList<>(); + private XMLTreeAction lastUndo; + private XMLTreeAction lastRedo; + private boolean undoing; + private boolean redoing; + private TreePath activePath; + /** * Default constructor. */ @@ -77,6 +86,10 @@ public XMLTreeModel(Document doc, Map nameMap) { this.nameMap = nameMap; childMap = new HashMap<>(); root = createRootAndCategoryNodes(); + lastUndo = null; + lastRedo = null; + undoing = false; + redoing = false; } /** @@ -267,6 +280,88 @@ public void removeTreeModelListener(TreeModelListener l) { } } + public void setActivePath(TreePath path) { + activePath = path; + } + + private void newAction(XMLTreeAction action) { + if ((lastUndo == null) || !((lastUndo.name.equals(action.name) && lastUndo.elem.equals(action.elem) && lastUndo.newValue.equals(action.oldValue)))) { + undoActions.addFirst(action); + + if (undoActions.size() > 50) { + undoActions.removeLast(); + } + + if (lastRedo != null && !((lastRedo.name.equals(action.name) && lastRedo.elem.equals(action.elem) && lastRedo.newValue.equals(action.oldValue)))) { + redoActions.clear(); + } + } else { + redoActions.addFirst(action); + + if (redoActions.size() > 50) { + redoActions.removeLast(); + } + } + } + + public void undoAction() { + if (!undoActions.isEmpty()) { + lastUndo = undoActions.removeFirst(); + undoing = true; + + switch (lastUndo.type) { + case XMLTreeAction.actionRemove: + XMLTreeNode parent = (XMLTreeNode)lastUndo.parentPath.getLastPathComponent(); + if (!parent.isType(OBJECTS)) { + TreePath parentPath = new TreePath(root); + parent = (XMLTreeNode) parentPath.getLastPathComponent(); + } + parent.actual().insertBefore(lastUndo.elem, lastUndo.nextSibling); + break; + case XMLTreeAction.actionInsert: + lastUndo.parentPath = getPath(lastUndo.elem).getParentPath(); + lastUndo.nextSibling = getNextSibling(lastUndo.elem, lastUndo.parentPath); + + lastUndo.elem.getParentNode().removeChild(lastUndo.elem); + break; + case XMLTreeAction.actionChange: + lastUndo.elem.setAttribute(lastUndo.name, lastUndo.oldValue); + break; + } + + fixModel(); + } + } + + public void redoAction() { + if (!redoActions.isEmpty()) { + lastRedo = redoActions.removeFirst(); + redoing = true; + + switch (lastRedo.type) { + case XMLTreeAction.actionRemove: + XMLTreeNode parent = (XMLTreeNode)lastRedo.parentPath.getLastPathComponent(); + if (!parent.isType(OBJECTS)) { + TreePath parentPath = new TreePath(root); + parent = (XMLTreeNode) parentPath.getLastPathComponent(); + } + parent.actual().insertBefore(lastRedo.elem, lastRedo.nextSibling); + break; + case XMLTreeAction.actionInsert: + lastRedo.parentPath = getPath(lastRedo.elem).getParentPath(); + lastRedo.nextSibling = getNextSibling(lastRedo.elem, lastRedo.parentPath); + + lastRedo.elem.getParentNode().removeChild(lastRedo.elem); + break; + case XMLTreeAction.actionChange: + lastRedo.elem.setAttribute(lastRedo.name, lastRedo.oldValue); + break; + } + + fixModel(); + } + } + //------------------------------------------------------------// /** @@ -539,15 +634,44 @@ public void run() { String type = mev.getType(); if (type.equals("DOMNodeRemoved")) { //System.out.println("REMOVING: " + target.getAttribute(NAME)); + TreePath parentPath = activePath.getParentPath(); + Element nextSibling = null; + + if (undoing) { + parentPath = lastUndo.parentPath; + nextSibling = lastUndo.nextSibling; + } else if (redoing) { + parentPath = lastRedo.parentPath; + nextSibling = lastRedo.nextSibling; + } else { + nextSibling = getNextSibling(target, parentPath); + } + + XMLTreeAction action = new XMLTreeAction(XMLTreeAction.actionRemove, target, parentPath, nextSibling, NAME, "", target.getAttribute(NAME)); + newAction(action); changeAttribute(target, NAME, "", target.getAttribute(NAME)); } else if (type.equals("DOMNodeInserted")) { //System.out.println("INSERTING: " + target.getAttribute(NAME)); changeAttribute(target, NAME, target.getAttribute(NAME), ""); - } - if (type.equals("DOMAttrModified")) { + + TreePath parentPath = activePath; + Element nextSibling = null; + + if (undoing) { + parentPath = lastUndo.parentPath; + nextSibling = lastUndo.nextSibling; + } else if (redoing) { + parentPath = lastRedo.parentPath; + nextSibling = lastRedo.nextSibling; + } + XMLTreeAction action = new XMLTreeAction(XMLTreeAction.actionInsert, target, parentPath, nextSibling, NAME, target.getAttribute(NAME), ""); + newAction(action); + } else if (type.equals("DOMAttrModified")) { String name = mev.getAttrName(); String newValue = mev.getNewValue(); String oldValue = mev.getPrevValue(); // can be null + XMLTreeAction action = new XMLTreeAction(XMLTreeAction.actionChange, target, null, null, name, newValue, oldValue); + newAction(action); changeAttribute(target, name, newValue, oldValue); if (!name.equals(NAME)) { return; @@ -555,6 +679,9 @@ public void run() { } fixModel(); + + undoing = false; + redoing = false; } }); } @@ -977,6 +1104,36 @@ static public int[] getAllIndices(List list) { } */ + public Element getNextSibling(Element element, TreePath parentPath) { + XMLTreeNode parentNode = (XMLTreeNode)parentPath.getLastPathComponent(); + XMLTreeNodeList c = getXMLTreeNodeList(parentNode); + for (int i = 0; i < c.size(); i++) { + if (element.getAttribute(NAME).equals(c.get(i).getName()) && i < (c.size() - 1)) { + return c.get(i + 1).effective(); + } + } + return null; + } + + public TreePath getPath(Element element) { + return getPath(element, new TreePath(root)); + } + + private TreePath getPath(Element element, TreePath path) { + XMLTreeNode node = (XMLTreeNode) path.getLastPathComponent(); + XMLTreeNodeList c = getXMLTreeNodeList(node); + for (XMLTreeNode n : c) { + if (n.actual()== element || n.link() == element) { + return path.pathByAddingChild(n); + } + TreePath nodePath = getPath(element, path.pathByAddingChild(n)); + if (nodePath != null) { + return nodePath; + } + } + return null; + } + /** * Tries to convert string path into TreePath of XMLTreeNodes. * @param pathStr