Below is the xml that we will be using to display in the Jtree.
CustomTreeNode
First lets implement our custom tree node by extending the DefaultMutableTreeNode. In this class we will define one property and will override two methods.
toString() -> to display a meaningful name in the tree node
isLeaf() -> to specify that this node is a leaf iff the tag name is file, non-leaf otherwise.
And we will define a new property 'loaded' to indicate whether the node has been loaded or not.
package org.gpl.xmltree; import javax.swing.tree.DefaultMutableTreeNode; import org.w3c.dom.Element; /** * * @author Naveed Quadri */ public class XMLNode extends DefaultMutableTreeNode { private String name; private String iconName; private boolean isFile; private boolean loaded; XMLNode(Object nodeObject) { super(nodeObject); name = ((Element) userObject).getAttribute("printableName"); iconName = ((Element) userObject).getAttribute("type"); isFile = ((Element) userObject).getTagName().equalsIgnoreCase("file"); } public boolean isLoaded() { return loaded; } public void setLoaded(boolean loaded) { this.loaded = loaded; } public String getIconName() { return iconName; } @Override public String toString() { return name; } @Override public boolean isLeaf() { return isFile; } }
Custom TreeModel
All the tutorials i found on the internet suggested implementing the TreeModel interface for displaying XML data. But I found extending the DefaultTreeModel much better as all of the work is already done for us. For lazy loading of children, we'll implement the TreeWillExpandListener, so that the child nodes if any can be loaded when requested. Once the node has been loaded we will set the loaded property in the XMLNode class to true, so that the we dont have load the node again n again.
package org.gpl.xmltree; import java.util.Vector; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.ExpandVetoException; import org.w3c.dom.*; /** * * @author Naveed Quadri */ public class XMLTreeModel extends DefaultTreeModel implements TreeWillExpandListener { /** * * @param parentNode * @return * @see http://www.developer.com/xml/article.php/10929_3731356_2/Displaying-XML-in-a-Swing-JTree.htm */ public static VectorgetChildElements(Node parentNode) { NodeList list = parentNode.getChildNodes(); Vector childNodes = new Vector (); for (int i = 0; i < list.getLength(); i++) { if (list.item(i).getNodeType() == Node.ELEMENT_NODE) { childNodes.add((Element) list.item(i));// = new XMLNode((Element) list.item(i)); } } return childNodes; } /** * * @param root */ public XMLTreeModel(XMLNode root) { super(root); setChildren(root, XMLTreeModel.getChildElements((Node) root.getUserObject())); } /** * * @param parentNode * @param childElements */ public void setChildren(XMLNode parentNode, Vector childElements) { if (childElements == null) { return; } //get the chld count int childCount = parentNode.getChildCount(); //set the node as loaded parentNode.setLoaded(true); //remove all old nodes from the parent if (childCount > 0) { for (int i = 0; i < childCount; i++) { removeNodeFromParent((DefaultMutableTreeNode) parentNode.getChildAt(0)); } } XMLNode node; //insert the nodes in the parent node for (int i = 0; i < childElements.size(); i++) { node = new XMLNode(childElements.get(i)); insertNodeInto(node, parentNode, i); } } /** * * @param event * @throws ExpandVetoException */ public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { //get the lazy node XMLNode lazyNode = (XMLNode) event.getPath().getLastPathComponent(); //node is already loaded, does'nt have to do it again. return. if (lazyNode.isLoaded()) { return; } //add the child nodes to the parent setChildren(lazyNode, XMLTreeModel.getChildElements((Node) lazyNode.getUserObject())); } public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { } }
Custom TreeCellRenderer
To display different icons for different type of nodes, we add HashTable as a client property to JTree. The Hashtable will map the iconType to the icon file .
////....... // jTree.putClientProperty("icons", getIcons()); // //........ // private Hashtable getIcons() { Hashtable icons = new Hashtable(); icons.put("unknown", IconFactory.getIcon("unknown", IconFactory.IconSize.SIZE_16X16)); icons.put("doc", IconFactory.getIcon("word", IconFactory.IconSize.SIZE_16X16)); icons.put("pdf", IconFactory.getIcon("adobe", IconFactory.IconSize.SIZE_16X16)); icons.put("docx", IconFactory.getIcon("word", IconFactory.IconSize.SIZE_16X16)); icons.put("dir", IconFactory.getIcon("dir", IconFactory.IconSize.SIZE_16X16)); icons.put("txt", IconFactory.getIcon("text", IconFactory.IconSize.SIZE_16X16)); icons.put("archive", IconFactory.getIcon("archive", IconFactory.IconSize.SIZE_16X16)); icons.put("media", IconFactory.getIcon("media", IconFactory.IconSize.SIZE_16X16)); icons.put("/", IconFactory.getIcon("usb", IconFactory.IconSize.SIZE_16X16)); return icons; }
IconFactory is the class to get the icons from the resources dir and resize it to 16X16. The source of this class can be found in the project archive attached at the end of the page.
In the TreeCellRenderer we retrieve the icons hashtable set the icon depending on the type of node.
package org.gpl.xmltree; import java.awt.Component; import java.util.Hashtable; import javax.swing.Icon; import javax.swing.JTree; import javax.swing.tree.DefaultTreeCellRenderer; /** * * @author Naveed Quadri */ public class XMLTreeCellRenderer extends DefaultTreeCellRenderer{ @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); Icon icon = null; //Retrieve the 'icons' clientProperty Hashtable icons = (Hashtable) tree.getClientProperty("icons"); //get the type of node String name = ((XMLNode) value).getIconName(); if ((name != null)) { //get the icon for this type of node icon = (Icon) icons.get(name); if(icon == null) //if we could'nt find anything, get the 'unknown' icon icon = (Icon)icons.get("unknown"); //set the icon setIcon(icon); } //set the tooltip setToolTipText(((XMLNode) value).toString()); // return back this component return this; } }
TransferableNode
To implement the drag and drop interface, XMLNode has to wrapped in a transferable object. we do that by implenting the transferable interface. The Transferable interface contains three methods that has to be implemented.
package org.gpl.xmltree; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; public class NodesTransferable implements Transferable { //specify the data flavor final public static DataFlavor INFO_FLAVOR = new DataFlavor(XMLNode[].class, "application/x-java-serialized-object"); static DataFlavor flavors[] = {INFO_FLAVOR}; XMLNode[] nodes; //an array of XMLNodes to support DnD for more than one node public NodesTransferable(XMLNode[] nodes) { this.nodes = nodes; } /** * * @param flavor * @return NodesTransferable object * @throws UnsupportedFlavorException */ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { if (!isDataFlavorSupported(flavor)) { throw new UnsupportedFlavorException(flavor); } return nodes; } //return the data flavors public DataFlavor[] getTransferDataFlavors() { return flavors; } public boolean isDataFlavorSupported(DataFlavor flavor) { return flavor.equals(INFO_FLAVOR); } }
TransferHandler
Now that we have our transferable object, we need a transfer handler that will transfer the node object from one position to other. we do that by extending the TransferHandler class.
package org.gpl.xmltree; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.util.ArrayList; import java.util.List; import javax.swing.JComponent; import javax.swing.JTree; import javax.swing.TransferHandler; import javax.swing.tree.TreePath; import org.w3c.dom.Node; /** * * @author Naveed Quadri * @see http://www.coderanch.com/t/346509/GUI/java/JTree-drag-drop-inside-one */ class TreeTransferHandler extends TransferHandler { private XMLNode[] nodesToRemove; public TreeTransferHandler() { } @Override public boolean canImport(TransferHandler.TransferSupport support) { if (!support.isDrop()) { return false; } support.setShowDropLocation(true); if (!support.isDataFlavorSupported(NodesTransferable.INFO_FLAVOR)) { return false; } // Do not allow a drop on the drag source selections. JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation(); TreePath dest = dl.getPath(); XMLNode target = (XMLNode) dest.getLastPathComponent(); JTree tree = (JTree) support.getComponent(); int dropRow = tree.getRowForPath(dl.getPath()); int[] selRows = tree.getSelectionRows(); for (int i = 0; i < selRows.length; i++) { if (selRows[i] == dropRow) { return false; } } if (target.isLeaf()) { return false; } // Do not allow MOVE-action drops if a non-leaf node is // selected unless all of its children are also selected. int action = support.getDropAction(); if (action == MOVE) { return haveCompleteNode(tree); } // Do not allow a non-leaf node to be copied to a level // which is less than its source level. TreePath path = tree.getPathForRow(selRows[0]); XMLNode firstNode = (XMLNode) path.getLastPathComponent(); if (firstNode.getChildCount() > 0 && target.getLevel() < firstNode.getLevel()) { return false; } return true; } private boolean haveCompleteNode(JTree tree) { int[] selRows = tree.getSelectionRows(); TreePath path = tree.getPathForRow(selRows[0]); XMLNode first = (XMLNode) path.getLastPathComponent(); int childCount = first.getChildCount(); // first has children and no children are selected. if (childCount > 0 && selRows.length == 1) { return false; } // first may have children. for (int i = 1; i < selRows.length; i++) { path = tree.getPathForRow(selRows[i]); XMLNode next = (XMLNode) path.getLastPathComponent(); if (first.isNodeChild(next)) { // Found a child of first. if (childCount > selRows.length - 1) { // Not all children of first are selected. return false; } } } return true; } @Override protected Transferable createTransferable(JComponent c) { JTree tree = (JTree) c; TreePath[] paths = tree.getSelectionPaths(); if (paths != null) { // Make up a node array of copies for transfer and // another for/of the nodes that will be removed in // exportDone after a successful drop. Listcopies = new ArrayList (); List toRemove = new ArrayList (); XMLNode node = (XMLNode) paths[0].getLastPathComponent(); if (!node.isLoaded()) { XMLTreeModel model = (XMLTreeModel) tree.getModel(); model.setChildren(node, XMLTreeModel.getChildElements((Node) node.getUserObject())); } //XMLNode will loose its loaded property after //making a copy of it. //so reading the property of the node before making //a copy and setting the property back after copying. boolean loaded = node.isLoaded(); XMLNode copy = copy(node); copy.setLoaded(loaded); copies.add(copy); toRemove.add(node); for (int i = 1; i < paths.length; i++) { XMLNode next = (XMLNode) paths[i].getLastPathComponent(); // Do not allow higher level nodes to be added to list. if (next.getLevel() < node.getLevel()) { break; } else if (next.getLevel() > node.getLevel()) { // child node copy.add(copy(next)); // node already contains child } else { // sibling copies.add(copy(next)); toRemove.add(next); } } XMLNode[] nodes = copies.toArray(new XMLNode[copies.size()]); nodesToRemove = toRemove.toArray(new XMLNode[toRemove.size()]); return new NodesTransferable(nodes); } return null; } /** Defensive copy used in createTransferable. */ private XMLNode copy(XMLNode node) { return new XMLNode(node.getUserObject()); } protected void exportDone(JComponent source, Transferable data, int action) { if ((action & MOVE) == MOVE) { JTree tree = (JTree) source; XMLTreeModel model = (XMLTreeModel) tree.getModel(); // Remove nodes saved in nodesToRemove in createTransferable. for (int i = 0; i < nodesToRemove.length; i++) { model.removeNodeFromParent(nodesToRemove[i]); } } } public int getSourceActions(JComponent c) { return COPY_OR_MOVE; } public boolean importData(TransferHandler.TransferSupport support) { if (!canImport(support)) { return false; } // Extract transfer data. XMLNode[] nodes = null; try { Transferable t = support.getTransferable(); nodes = (XMLNode[]) t.getTransferData(NodesTransferable.INFO_FLAVOR); } catch (UnsupportedFlavorException ufe) { System.out.println("UnsupportedFlavor: " + ufe.getMessage()); } catch (java.io.IOException ioe) { System.out.println("I/O error: " + ioe.getMessage()); } // Get drop location info. JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation(); int childIndex = dl.getChildIndex(); TreePath dest = dl.getPath(); XMLNode parent = (XMLNode) dest.getLastPathComponent(); JTree tree = (JTree) support.getComponent(); XMLTreeModel model = (XMLTreeModel) tree.getModel(); if (!parent.isLoaded()) { model.setChildren(parent, XMLTreeModel.getChildElements((Node) parent.getUserObject())); } // Configure for drop mode. int index = childIndex; // DropMode.INSERT if (childIndex == -1) { // DropMode.ON index = parent.getChildCount(); } // Add data to model. for (int i = 0; i < nodes.length; i++) { model.insertNodeInto(nodes[i], parent, index++); } return true; } public String toString() { return getClass().getName(); } }
XMLTree
Create a XMLTree class by extending the JTree. Get the path to the xml file in the constructor . Get the instance of the DocumentBuilderFactory to parse the xml file and get the root node of the xml. Instantiate and set the TreeModel and TreeCellRenderer. Add TreeWillExpandListener and put the client property for the icons.
package org.gpl.xmltree; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.util.Hashtable; import java.util.Vector; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.DropMode; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.JTree; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.*; import org.xml.sax.SAXException; /** * * @author Naveed Quadri */ public class XMLTree extends JTree { private XMLTreeModel xmlTreeModel; private XMLNode root; private XMLTreeCellRenderer xmlTreeCellRenderer; private Document xmlDoc; private AbstractAction expandCollapseAction; private JPopupMenu popupMenu; private TreePath clickedPath; public XMLTree(String xmlFile) { try { root = getRoot(xmlFile); } catch (ParserConfigurationException ex) { } catch (SAXException ex) { } catch (IOException ex) { } catch (NullPointerException ex) { } xmlTreeModel = new XMLTreeModel(root); xmlTreeCellRenderer = new XMLTreeCellRenderer(); //set the treemodel setModel(xmlTreeModel); setShowsRootHandles(true); //add Tree Will Expand Listener addTreeWillExpandListener(xmlTreeModel); //enable drag n drop setDragEnabled(true); setDropMode(DropMode.ON_OR_INSERT); // setTransferHandler(new TreeTransferHandler()); getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); putClientProperty("JTree.lineStyle", "Angled"); // put Client Property for icons putClientProperty("icons", getIcons()); // set the tree cell renderer setCellRenderer(xmlTreeCellRenderer); //attach the popup popupMenu = getJPopupForExplorerTree(); add(popupMenu); // add the mouse listener for showing the popup addMouseListener(new MouseAdapter() { @Override public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { //get the co-ord of the mouse pointer int x = e.getX(); int y = e.getY(); clickedPath = getPathForLocation(x, y); if (clickedPath != null) { XMLNode node = (XMLNode) clickedPath.getLastPathComponent(); //org.w3c.dom.Element el = node.getElement(); if (!node.isLeaf()) { if (isExpanded(clickedPath)) { expandCollapseAction.putValue(Action.NAME, "Collapse"); } else { expandCollapseAction.putValue(Action.NAME, "Expand"); } } else { expandCollapseAction.putValue(Action.NAME, "Open"); } popupMenu.show((JComponent) e.getSource(), e.getX(), e.getY()); setSelectionPath(clickedPath); } } } }); } private JPopupMenu getJPopupForExplorerTree() { AbstractAction copyAction; AbstractAction cutAction; AbstractAction pasteAction; AbstractAction deleteAction; JPopupMenu popup = new JPopupMenu(); expandCollapseAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { if (clickedPath == null) { return; } if (isExpanded(clickedPath)) { collapsePath(clickedPath); } else { expandPath(clickedPath); } } }; copyAction = new AbstractAction("Copy", IconFactory.getIcon("copy", IconFactory.IconSize.SIZE_16X16)) { public void actionPerformed(ActionEvent e) { System.out.println("Copy"); } }; cutAction = new AbstractAction("Cut", IconFactory.getIcon("cut", IconFactory.IconSize.SIZE_16X16)) { public void actionPerformed(ActionEvent e) { System.out.println("Cut"); } }; pasteAction = new AbstractAction("Paste", IconFactory.getIcon("paste", IconFactory.IconSize.SIZE_16X16)) { public void actionPerformed(ActionEvent e) { System.out.println("Paste"); } }; deleteAction = new AbstractAction("Delete", IconFactory.getIcon("delete", IconFactory.IconSize.SIZE_16X16)) { public void actionPerformed(ActionEvent e) { System.out.println("Delete"); } }; popup.add(expandCollapseAction); popup.addSeparator(); popup.add(copyAction); popup.add(cutAction); popup.add(pasteAction); popup.add(deleteAction); return popup; } private Hashtable getIcons() { //Now the XMLTree can be instantiated by// see Custom TreeCellRenderer // } private XMLNode getRoot(String xmlFile) throws ParserConfigurationException, SAXException, IOException { Vector elements; DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dbFactory.newDocumentBuilder(); xmlDoc = builder.parse(new File(xmlFile)); xmlDoc.normalize(); elements = XMLTreeModel.getChildElements(xmlDoc); if (elements.size() > 0) { return new XMLNode(elements.get(0)); } return null; } }
XMLTree xmlTree = new XMLTree("path\\to\\xml\\file.xml");
A complete NetBeans project can be downloaded from here
Create Post thank you very much!
ReplyDeleteHello I got question about your project.
ReplyDeleteis your project contain the way to save your
change in your xml file?
I used org.w3g.dom thing to find a way to save
into the document object, but not yet succeed.
so I looking for a way. your's seems like using
SAX. Hopefully can you explain to me how you
save your change to xml? that will be very
helpful to my project. Thank you
@jangho
ReplyDeleteNo I have not implemented saving xml files in this project.
You may already have visited this link, if not check this out java2s
if it still doesnt work let me know, i can try it for you.
@jangho also i am using dom in this project, SAX is read only
ReplyDelete@Naveed Thanks for your reply.
ReplyDeleteI'm implementing the way to save into dom file in
the importdata, in the transferhandler. and
making another constructor
to get the Dom file in the transferhandler
currently I can't find how can I change the data
in the dom file when I DnD the data.
It shows good on the JTree but doesn't fix the
xml, when I reload it.
I wish I can get little help to figuring out
Thanks
the object I tried to manipulate is the
ReplyDeleteorg.w3g.dom.document object all I need to implementing is about change document when I change tree.
jangho, right now i am out of station so i dont have my laptop handy so i cant check for myself..
ReplyDeletebut a few pointers..
implement TreeModelListener so when ever your model changes you are notified about it. and whenever model changes serialize the model itself. I think this shud give you an xml which you can deserialize later and construct your treemodel back. Or you can also use TransformerFactory.
Document doc = you are getting this in your constructor
TransformerFactory tf = TransformerFactory.newInstance();
Transformer output = tf.newTransformer();
output.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes");
output.transform(new DOMSource(doc),new StreamResult(file));
if your using an IDE it will fix your imports otherwise you can get them from javadocs, coz even i am not sure of the imports.
tell me how this goes. if you found an alternate or better solution, i will be glad if you cud update me.
I am currently trying to implements DropTargetListener and DropSourcelistener and DropGestureListener, and try to mix it with my TransferHandler.
ReplyDeleteHowever I have no gurantee of success yet.
I'll try your first suggestion later.
and I can't try the second one cause I cannot
directly get a access to the xml file in the server.
all I get is the document object from server
and I need to manipulate it
Anyway I'm trying to capture the object at the oment of drop with those listeners.
If you have any suggestion or hint in that way, then let me know please
I'm eager to get any suggestion or hint.
Thank you for your kindly reply.
Hi Naveed,
ReplyDeleteI'm trying to implements the rename of the XMLNode (printablename attribute).
Can you tell me which is the best way for to do this?
I have implemented the extended TreeModelListener class, I have the UserObject name
changed but I don't see how to set the new attribute name in XMLNode.
Many thanks
Luigi