/*
* 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.codename1.ui.tree;
import com.codename1.components.SpanButton;
import com.codename1.ui.Button;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Display;
import com.codename1.ui.FontImage;
import com.codename1.ui.Image;
import com.codename1.ui.Label;
import com.codename1.ui.animations.CommonTransitions;
import com.codename1.ui.animations.Transition;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.plaf.Style;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.EventDispatcher;
import java.util.Vector;
/**
* <p>The {@code Tree} component allows constructing simple tree component hierarchies that can be expanded
* seamlessly 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.<br>
* To customize the look of the tree the component can be derived and component creation can be replaced.</p>
*
* <script src="https://gist.github.com/codenameone/870d4412694bca3092c4.js"></script>
* <img src="https://www.codenameone.com/img/developer-guide/tree.png" alt="Tree sample code" />
*
* <p>
* And heres a more "real world" example showing an XML hierarchy in a {@code Tree}:
* </p>
* <script src="https://gist.github.com/codenameone/5361ad7339c1ae26e0b8.js"></script>
* <img src="https://www.codenameone.com/img/developer-guide/components-tree-xml.png" alt="Tree with XML data" />
*
* <p>
* Another real world example showing the {@link com.codename1.io.FileSystemStorage} as a tree:
* </p>
* <script src="https://gist.github.com/codenameone/2877412809a8cff646af.js"></script>
* <img src="https://www.codenameone.com/img/developer-guide/filesystem-tree.png" alt="Simple sample of a tree for the FileSystemStorage API">
*
* @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;
private boolean multilineMode;
/**
* 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"}
}));
}
/**
* Toggles a mode where rows in the tree can be broken since span buttons will
* be used instead of plain buttons.
* @return the multilineMode
*/
public boolean isMultilineMode() {
return multilineMode;
}
/**
* Toggles a mode where rows in the tree can be broken since span buttons will
* be used instead of plain buttons.
* @param multilineMode the multilineMode to set
*/
public void setMultilineMode(boolean multilineMode) {
this.multilineMode = multilineMode;
}
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();
int a0len = arr[0].length;
for(int iter = 0 ; iter < a0len ; iter++) {
v.addElement(arr[0][iter]);
}
return v;
}
int alen = arr.length;
int aolen = arr[0].length;
Vector v = new Vector();
for(int iter = 0 ; iter < aolen ; iter++) {
if(parent == arr[0][iter]) {
if(alen > iter + 1 && arr[iter + 1] != null) {
int ailen = arr[iter + 1].length;
for(int i = 0 ; i < ailen ; 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[] {com.codename1.impl.CodenameOneImplementation.getStringArray2DClass()};
}
/**
* {@inheritDoc}
*/
public String[] getPropertyTypeNames() {
return new String[] {"String[][]"};
}
/**
* {@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));
if(folder == null) {
folder = UIManager.getInstance().getThemeImageConstant("treeFolderImage");
openFolder = UIManager.getInstance().getThemeImageConstant("treeFolderOpenImage");
nodeImage = UIManager.getInstance().getThemeImageConstant("treeNodeImage");
}
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 Container expandNode(boolean animate, Component c) {
return expandNodeImpl(animate, c);
}
private Container expandNodeImpl(boolean animate, Component c) {
Container p = c.getParent().getLeadParent();
if(p != null) {
c = p;
}
c.putClientProperty(KEY_EXPANDED, "true");
if(openFolder == null) {
FontImage.setMaterialIcon(c, FontImage.MATERIAL_FOLDER, 3);
} else {
setNodeIcon(openFolder, c);
}
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));
parent.addComponent(BorderLayout.CENTER, dest);
buildBranch(o, depth, dest);
if(isInitialized() && animate) {
// prevent a race condition on node expansion contraction
parent.animateHierarchyAndWait(300);
if(multilineMode) {
revalidate();
}
} else {
parent.revalidate();
}
return dest;
}
/**
* This method returns true if the given node is expanded.
* @param node a Component that represents a tree node.
* @return true if this tree node is expanded
*/
protected boolean isExpanded(Component node) {
Object e = node.getClientProperty(KEY_EXPANDED);
return e != null && e.equals("true");
}
private Container expandPathNode(boolean animate, Container parent, Object node) {
int cc = parent.getComponentCount();
for(int iter = 0 ; iter < cc ; iter++) {
Component current = parent.getComponentAt(iter);
Object o = current.getClientProperty(KEY_OBJECT);
if(!model.isLeaf(o)){
//if(current instanceof Container) {
BorderLayout bl = (BorderLayout)((Container)current).getLayout();
// the tree component is always at north expanded or otherwise
current = bl.getNorth();
if(o == node || o != null && o.equals(node)) {
if(isExpanded(current)) {
return (Container)bl.getCenter();
}
return expandNodeImpl(animate, current);
}
}
}
return null;
}
private void collapsePathNode(Container parent, Object node) {
int cc = parent.getComponentCount();
for(int iter = 0 ; iter < cc ; iter++) {
Component current = parent.getComponentAt(iter);
if(isExpanded(current)) {
BorderLayout bl = (BorderLayout)((Container)current).getLayout();
// the tree component is always at north expanded or otherwise
current = bl.getNorth();
Object o = current.getClientProperty(KEY_OBJECT);
if(o != null && o.equals(node)) {
if(isExpanded(current)) {
collapseNode(current, null);
}
return;
}
}
}
}
/**
* Expands the tree path
* @param path the path to expand
*/
public void expandPath(Object... path) {
expandPath(isInitialized(), path);
}
/**
* Expands the tree path
* @param path the path to expand
* @param animate whether to animate expansion
*/
public void expandPath(boolean animate, Object... path) {
Container c = this;
int plen = path.length;
for(int iter = 0 ; iter < plen ; iter++) {
c = expandPathNode(animate, c, path[iter]);
if(c == null) {
return;
}
}
}
/**
* Collapses the last element in the path
* @param path the path to the element that should be collapsed
*/
public void collapsePath(Object... path) {
Container c = this;
int plen = path.length;
for(int iter = 0 ; iter < plen - 1; iter++) {
c = expandPathNode(isInitialized(), c, path[iter]);
}
collapsePathNode(c, path[plen - 1]);
}
private void collapseNode(Component c) {
collapseNode(c, CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, false, 300));
}
private void collapseNode(Component c, Transition t) {
Container lead = c.getParent().getLeadParent();
if(lead != null) {
c = lead;
}
c.putClientProperty(KEY_EXPANDED, null);
setNodeIcon(folder, c);
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, t, 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);
Component nodeComponent = createNode(current, depth);
if(model.isLeaf(current)) {
destination.addComponent(nodeComponent);
bindNodeListener(new Handler(current), nodeComponent);
} else {
Container componentArea = new Container(new BorderLayout());
componentArea.addComponent(BorderLayout.NORTH, nodeComponent);
destination.addComponent(componentArea);
bindNodeListener(expansionListener, nodeComponent);
}
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
* @deprecated replaced with createNode, bindNodeListener and setNodeIcon
*/
protected Button createNodeComponent(Object node, int depth) {
Button cmp = new Button(childToDisplayLabel(node));
cmp.setUIID("TreeNode");
if(model.isLeaf(node)) {
if(nodeImage == null) {
FontImage.setMaterialIcon(cmp, FontImage.MATERIAL_DESCRIPTION, 3);
} else {
cmp.setIcon(nodeImage);
}
} else {
if(folder == null) {
FontImage.setMaterialIcon(cmp, FontImage.MATERIAL_FOLDER, 3);
} else {
cmp.setIcon(folder);
}
}
updateNodeComponentStyle(cmp.getSelectedStyle(), depth);
updateNodeComponentStyle(cmp.getUnselectedStyle(), depth);
updateNodeComponentStyle(cmp.getPressedStyle(), depth);
return cmp;
}
/**
* Since a node may be any component type developers should override this method to
* add support for binding the click listener to the given component.
* @param l listener interface
* @param node node component returned by createNode
*/
protected void bindNodeListener(ActionListener l, Component node) {
if(node instanceof Button) {
((Button)node).addActionListener(l);
return;
}
((SpanButton)node).addActionListener(l);
}
/**
* Sets the icon for the given node similar in scope to bindNodeListener
* @param icon the icon for the node
* @param node the node instance
*/
protected void setNodeIcon(Image icon, Component node) {
if(node instanceof Button) {
((Button)node).setIcon(icon);
return;
}
((SpanButton)node).setIcon(icon);
}
/**
* 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 Component createNode(Object node, int depth) {
if(multilineMode) {
SpanButton cmp = new SpanButton(childToDisplayLabel(node));
cmp.setUIID("TreeNode");
cmp.setTextUIID("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;
}
return createNodeComponent(node, depth);
}
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 = Math.max(1, 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,ActionEvent.Type.Other));
return;
}
Component c = (Component)evt.getSource();
Container lead = c.getParent().getLeadParent();
if(lead != null) {
c = lead;
}
Object e = c.getClientProperty(KEY_EXPANDED);
if(e != null && e.equals("true")) {
collapseNode(c);
} else {
expandNode(isInitialized(), c);
}
}
}
}