/* * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores * CA 94065 USA or visit www.oracle.com if you need additional information or * have any questions. */ package com.sun.lwuit.tree; import com.sun.lwuit.Button; import com.sun.lwuit.Component; import com.sun.lwuit.Container; import com.sun.lwuit.Display; import com.sun.lwuit.Image; import com.sun.lwuit.Label; import com.sun.lwuit.animations.CommonTransitions; import com.sun.lwuit.events.ActionEvent; import com.sun.lwuit.events.ActionListener; import com.sun.lwuit.geom.Dimension; import com.sun.lwuit.layouts.BorderLayout; import com.sun.lwuit.layouts.BoxLayout; import com.sun.lwuit.plaf.Style; import com.sun.lwuit.util.EventDispatcher; import java.util.Vector; /** * The tree component allows constructing simple tree component hierechies that can be expaneded seamingly * with no limit. The tree is bound to a model that can provide data with free form depth such as file system * or similarly structured data. * To customize the look of the tree the component can be derived and component creation can be replaced. * * @author Shai Almog */ public class Tree extends Container { private static final String KEY_OBJECT = "TREE_OBJECT"; private static final String KEY_PARENT = "TREE_PARENT"; private static final String KEY_EXPANDED = "TREE_NODE_EXPANDED"; private static final String KEY_DEPTH = "TREE_DEPTH"; private EventDispatcher leafListener = new EventDispatcher(); private ActionListener expansionListener = new Handler(); private TreeModel model; private static Image folder; private static Image openFolder; private static Image nodeImage; private int depthIndent = 15; /** * Constructor for usage by GUI builder and automated tools, normally one * should use the version that accepts the model */ public Tree() { this(new StringArrayTreeModel(new String[][] { {"Colors", "Letters", "Numbers"}, {"Red", "Green", "Blue"}, {"A", "B", "C"}, {"1", "2", "3"} })); } static class StringArrayTreeModel implements TreeModel { String[][] arr; StringArrayTreeModel(String[][] arr) { this.arr = arr; } public Vector getChildren(Object parent) { if(parent == null) { Vector v = new Vector(); for(int iter = 0 ; iter < arr[0].length ; iter++) { v.addElement(arr[0][iter]); } return v; } Vector v = new Vector(); for(int iter = 0 ; iter < arr[0].length ; iter++) { if(parent == arr[0][iter]) { if(arr.length > iter + 1 && arr[iter + 1] != null) { for(int i = 0 ; i < arr[iter + 1].length ; i++) { v.addElement(arr[iter + 1][i]); } } } } return v; } public boolean isLeaf(Object node) { Vector v = getChildren(node); return v == null || v.size() == 0; } } /** * @inheritDoc */ public String[] getPropertyNames() { return new String[] {"data"}; } /** * @inheritDoc */ public Class[] getPropertyTypes() { return new Class[] {String[][].class}; } /** * @inheritDoc */ public Object getPropertyValue(String name) { if(name.equals("data")) { return ((StringArrayTreeModel)model).arr; } return null; } /** * @inheritDoc */ public String setPropertyValue(String name, Object value) { if(name.equals("data")) { setModel(new StringArrayTreeModel((String[][])value)); return null; } return super.setPropertyValue(name, value); } /** * Construct a tree with the given tree model * * @param model represents the contents of the tree */ public Tree(TreeModel model) { this.model = model; setLayout(new BoxLayout(BoxLayout.Y_AXIS)); buildBranch(null, 0, this); setScrollableY(true); setUIID("Tree"); } /** * Returns the tree model instance * * @return the tree model */ public TreeModel getModel() { return model; } /** * Sets the tree model to a new value * * @param model the model of the tree */ public void setModel(TreeModel model) { this.model = model; removeAll(); buildBranch(null, 0, this); } /** * Sets the icon for a tree folder * * @param folderIcon the icon for a folder within the tree */ public static void setFolderIcon(Image folderIcon) { folder = folderIcon; } /** * Sets the icon for a tree folder in its expanded state * * @param folderIcon the icon for a folder within the tree */ public static void setFolderOpenIcon(Image folderIcon) { openFolder = folderIcon; } /** * Sets the icon for a tree node * * @param nodeIcon the icon for a node within the tree */ public static void setNodeIcon(Image nodeIcon) { nodeImage = nodeIcon; } private void expandNode(Component c) { c.putClientProperty(KEY_EXPANDED, "true"); ((Button)c).setIcon(openFolder); int depth = ((Integer)c.getClientProperty(KEY_DEPTH)).intValue(); Container parent = c.getParent(); Object o = c.getClientProperty(KEY_OBJECT); Container dest = new Container(new BoxLayout(BoxLayout.Y_AXIS)); Label dummy = new Label(); parent.addComponent(BorderLayout.CENTER, dummy); buildBranch(o, depth, dest); parent.replace(dummy, dest, CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, true, 300)); } private void collapseNode(Component c) { c.putClientProperty(KEY_EXPANDED, null); ((Button)c).setIcon(folder); Container p = c.getParent(); for(int iter = 0 ; iter < p.getComponentCount() ; iter++) { if(p.getComponentAt(iter) != c) { Label dummy = new Label(); p.replaceAndWait(p.getComponentAt(iter), dummy, CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, false, 300), true); p.removeComponent(dummy); } } } /** * Returns the currently selected item in the tree * * @return the object selected within the tree */ public Object getSelectedItem() { Component c = getComponentForm().getFocused(); if(c != null) { return c.getClientProperty(KEY_OBJECT); } return null; } /** * Adds the child components of a tree branch to the given container. */ private void buildBranch(Object parent, int depth, Container destination) { Vector children = model.getChildren(parent); int size = children.size(); Integer depthVal = new Integer(depth + 1); for(int iter = 0 ; iter < size ; iter++) { final Object current = children.elementAt(iter); Button nodeComponent = createNodeComponent(current, depth); if(model.isLeaf(current)) { destination.addComponent(nodeComponent); nodeComponent.addActionListener(new Handler(current)); } else { Container componentArea = new Container(new BorderLayout()); componentArea.addComponent(BorderLayout.NORTH, nodeComponent); destination.addComponent(componentArea); nodeComponent.addActionListener(expansionListener); } nodeComponent.putClientProperty(KEY_OBJECT, current); nodeComponent.putClientProperty(KEY_PARENT, parent); nodeComponent.putClientProperty(KEY_DEPTH, depthVal); } } /** * Creates a node within the tree, this method is protected allowing tree to be * subclassed to replace the rendering logic of individual tree buttons. * * @param node the node object from the model to display on the button * @param depth the depth within the tree (normally represented by indenting the entry) * @return a button representing the node within the tree */ protected Button createNodeComponent(Object node, int depth) { Button cmp = new Button(childToDisplayLabel(node)); cmp.setUIID("TreeNode"); if(model.isLeaf(node)) { cmp.setIcon(nodeImage); } else { cmp.setIcon(folder); } updateNodeComponentStyle(cmp.getSelectedStyle(), depth); updateNodeComponentStyle(cmp.getUnselectedStyle(), depth); updateNodeComponentStyle(cmp.getPressedStyle(), depth); return cmp; } private void updateNodeComponentStyle(Style s, int depth) { s.setMargin(LEFT, depth * depthIndent); } /** * Converts a tree child to a label, this method can be overriden for * simple rendering effects * * @return a string representing the given tree node */ protected String childToDisplayLabel(Object child) { return child.toString(); } /** * A listener that fires when a leaf is clicked * * @param l listener to fire when the leaf is clicked */ public void addLeafListener(ActionListener l) { leafListener.addListener(l); } /** * Removes the listener that fires when a leaf is clicked * * @param l listener to remove */ public void removeLeafListener(ActionListener l) { leafListener.removeListener(l); } /** * @inheritDoc */ protected Dimension calcPreferredSize() { Dimension d = super.calcPreferredSize(); // if the tree is entirely collapsed try to reserve at least 6 rows for the content int count = getComponentCount(); for(int iter = 0 ; iter < count ; iter++) { if(getComponentAt(iter) instanceof Container) { return d; } } int size = model.getChildren(null).size(); if(size < 6) { return new Dimension(Math.max(d.getWidth(), Display.getInstance().getDisplayWidth() / 4 * 3), d.getHeight() / size * 6); } return d; } /** * This class unifies two action listeners into a single class to reduce the size overhead */ private class Handler implements ActionListener { private Object current; public Handler() { } public Handler(Object current) { this.current = current; } public void actionPerformed(ActionEvent evt) { if(current != null) { leafListener.fireActionEvent(new ActionEvent(current)); return; } Component c = (Component)evt.getSource(); Object e = c.getClientProperty(KEY_EXPANDED); if(e != null && e.equals("true")) { collapseNode(c); } else { expandNode(c); } } } }