Home | History | Annotate | Download | only in ui
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  ******************************************************************************/
     16 
     17 package com.badlogic.gdx.scenes.scene2d.ui;
     18 
     19 import com.badlogic.gdx.graphics.Color;
     20 import com.badlogic.gdx.graphics.g2d.Batch;
     21 import com.badlogic.gdx.scenes.scene2d.Actor;
     22 import com.badlogic.gdx.scenes.scene2d.Group;
     23 import com.badlogic.gdx.scenes.scene2d.InputEvent;
     24 import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent;
     25 import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
     26 import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
     27 import com.badlogic.gdx.scenes.scene2d.utils.Layout;
     28 import com.badlogic.gdx.scenes.scene2d.utils.Selection;
     29 import com.badlogic.gdx.scenes.scene2d.utils.UIUtils;
     30 import com.badlogic.gdx.utils.Array;
     31 
     32 /** A tree widget where each node has an icon, actor, and child nodes.
     33  * <p>
     34  * The preferred size of the tree is determined by the preferred size of the actors for the expanded nodes.
     35  * <p>
     36  * {@link ChangeEvent} is fired when the selected node changes.
     37  * @author Nathan Sweet */
     38 public class Tree extends WidgetGroup {
     39 	TreeStyle style;
     40 	final Array<Node> rootNodes = new Array();
     41 	final Selection<Node> selection;
     42 	float ySpacing = 4, iconSpacingLeft = 2, iconSpacingRight = 2, padding = 0, indentSpacing;
     43 	private float leftColumnWidth, prefWidth, prefHeight;
     44 	private boolean sizeInvalid = true;
     45 	private Node foundNode;
     46 	Node overNode;
     47 	private ClickListener clickListener;
     48 
     49 	public Tree (Skin skin) {
     50 		this(skin.get(TreeStyle.class));
     51 	}
     52 
     53 	public Tree (Skin skin, String styleName) {
     54 		this(skin.get(styleName, TreeStyle.class));
     55 	}
     56 
     57 	public Tree (TreeStyle style) {
     58 		selection = new Selection();
     59 		selection.setActor(this);
     60 		selection.setMultiple(true);
     61 		setStyle(style);
     62 		initialize();
     63 	}
     64 
     65 	private void initialize () {
     66 		addListener(clickListener = new ClickListener() {
     67 			public void clicked (InputEvent event, float x, float y) {
     68 				Node node = getNodeAt(y);
     69 				if (node == null) return;
     70 				if (node != getNodeAt(getTouchDownY())) return;
     71 				if (selection.getMultiple() && selection.hasItems() && UIUtils.shift()) {
     72 					// Select range (shift).
     73 					float low = selection.getLastSelected().actor.getY();
     74 					float high = node.actor.getY();
     75 					if (!UIUtils.ctrl()) selection.clear();
     76 					if (low > high)
     77 						selectNodes(rootNodes, high, low);
     78 					else
     79 						selectNodes(rootNodes, low, high);
     80 					selection.fireChangeEvent();
     81 					return;
     82 				}
     83 				if (node.children.size > 0 && (!selection.getMultiple() || !UIUtils.ctrl())) {
     84 					// Toggle expanded.
     85 					float rowX = node.actor.getX();
     86 					if (node.icon != null) rowX -= iconSpacingRight + node.icon.getMinWidth();
     87 					if (x < rowX) {
     88 						node.setExpanded(!node.expanded);
     89 						return;
     90 					}
     91 				}
     92 				if (!node.isSelectable()) return;
     93 				selection.choose(node);
     94 			}
     95 
     96 			public boolean mouseMoved (InputEvent event, float x, float y) {
     97 				setOverNode(getNodeAt(y));
     98 				return false;
     99 			}
    100 
    101 			public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) {
    102 				super.exit(event, x, y, pointer, toActor);
    103 				if (toActor == null || !toActor.isDescendantOf(Tree.this)) setOverNode(null);
    104 			}
    105 		});
    106 	}
    107 
    108 	public void setStyle (TreeStyle style) {
    109 		this.style = style;
    110 		indentSpacing = Math.max(style.plus.getMinWidth(), style.minus.getMinWidth()) + iconSpacingLeft;
    111 	}
    112 
    113 	public void add (Node node) {
    114 		insert(rootNodes.size, node);
    115 	}
    116 
    117 	public void insert (int index, Node node) {
    118 		remove(node);
    119 		node.parent = null;
    120 		rootNodes.insert(index, node);
    121 		node.addToTree(this);
    122 		invalidateHierarchy();
    123 	}
    124 
    125 	public void remove (Node node) {
    126 		if (node.parent != null) {
    127 			node.parent.remove(node);
    128 			return;
    129 		}
    130 		rootNodes.removeValue(node, true);
    131 		node.removeFromTree(this);
    132 		invalidateHierarchy();
    133 	}
    134 
    135 	/** Removes all tree nodes. */
    136 	public void clearChildren () {
    137 		super.clearChildren();
    138 		setOverNode(null);
    139 		rootNodes.clear();
    140 		selection.clear();
    141 	}
    142 
    143 	public Array<Node> getNodes () {
    144 		return rootNodes;
    145 	}
    146 
    147 	public void invalidate () {
    148 		super.invalidate();
    149 		sizeInvalid = true;
    150 	}
    151 
    152 	private void computeSize () {
    153 		sizeInvalid = false;
    154 		prefWidth = style.plus.getMinWidth();
    155 		prefWidth = Math.max(prefWidth, style.minus.getMinWidth());
    156 		prefHeight = getHeight();
    157 		leftColumnWidth = 0;
    158 		computeSize(rootNodes, indentSpacing);
    159 		leftColumnWidth += iconSpacingLeft + padding;
    160 		prefWidth += leftColumnWidth + padding;
    161 		prefHeight = getHeight() - prefHeight;
    162 	}
    163 
    164 	private void computeSize (Array<Node> nodes, float indent) {
    165 		float ySpacing = this.ySpacing;
    166 		float spacing = iconSpacingLeft + iconSpacingRight;
    167 		for (int i = 0, n = nodes.size; i < n; i++) {
    168 			Node node = nodes.get(i);
    169 			float rowWidth = indent + iconSpacingRight;
    170 			Actor actor = node.actor;
    171 			if (actor instanceof Layout) {
    172 				Layout layout = (Layout)actor;
    173 				rowWidth += layout.getPrefWidth();
    174 				node.height = layout.getPrefHeight();
    175 				layout.pack();
    176 			} else {
    177 				rowWidth += actor.getWidth();
    178 				node.height = actor.getHeight();
    179 			}
    180 			if (node.icon != null) {
    181 				rowWidth += spacing + node.icon.getMinWidth();
    182 				node.height = Math.max(node.height, node.icon.getMinHeight());
    183 			}
    184 			prefWidth = Math.max(prefWidth, rowWidth);
    185 			prefHeight -= node.height + ySpacing;
    186 			if (node.expanded) computeSize(node.children, indent + indentSpacing);
    187 		}
    188 	}
    189 
    190 	public void layout () {
    191 		if (sizeInvalid) computeSize();
    192 		layout(rootNodes, leftColumnWidth + indentSpacing + iconSpacingRight, getHeight() - ySpacing / 2);
    193 	}
    194 
    195 	private float layout (Array<Node> nodes, float indent, float y) {
    196 		float ySpacing = this.ySpacing;
    197 		for (int i = 0, n = nodes.size; i < n; i++) {
    198 			Node node = nodes.get(i);
    199 			Actor actor = node.actor;
    200 			float x = indent;
    201 			if (node.icon != null) x += node.icon.getMinWidth();
    202 			y -= node.height;
    203 			node.actor.setPosition(x, y);
    204 			y -= ySpacing;
    205 			if (node.expanded) y = layout(node.children, indent + indentSpacing, y);
    206 		}
    207 		return y;
    208 	}
    209 
    210 	public void draw (Batch batch, float parentAlpha) {
    211 		Color color = getColor();
    212 		batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
    213 		if (style.background != null) style.background.draw(batch, getX(), getY(), getWidth(), getHeight());
    214 		draw(batch, rootNodes, leftColumnWidth);
    215 		super.draw(batch, parentAlpha); // Draw actors.
    216 	}
    217 
    218 	/** Draws selection, icons, and expand icons. */
    219 	private void draw (Batch batch, Array<Node> nodes, float indent) {
    220 		Drawable plus = style.plus, minus = style.minus;
    221 		float x = getX(), y = getY();
    222 		for (int i = 0, n = nodes.size; i < n; i++) {
    223 			Node node = nodes.get(i);
    224 			Actor actor = node.actor;
    225 
    226 			if (selection.contains(node) && style.selection != null) {
    227 				style.selection.draw(batch, x, y + actor.getY() - ySpacing / 2, getWidth(), node.height + ySpacing);
    228 			} else if (node == overNode && style.over != null) {
    229 				style.over.draw(batch, x, y + actor.getY() - ySpacing / 2, getWidth(), node.height + ySpacing);
    230 			}
    231 
    232 			if (node.icon != null) {
    233 				float iconY = actor.getY() + Math.round((node.height - node.icon.getMinHeight()) / 2);
    234 				batch.setColor(actor.getColor());
    235 				node.icon.draw(batch, x + node.actor.getX() - iconSpacingRight - node.icon.getMinWidth(), y + iconY,
    236 					node.icon.getMinWidth(), node.icon.getMinHeight());
    237 				batch.setColor(Color.WHITE);
    238 			}
    239 
    240 			if (node.children.size == 0) continue;
    241 
    242 			Drawable expandIcon = node.expanded ? minus : plus;
    243 			float iconY = actor.getY() + Math.round((node.height - expandIcon.getMinHeight()) / 2);
    244 			expandIcon.draw(batch, x + indent - iconSpacingLeft, y + iconY, expandIcon.getMinWidth(), expandIcon.getMinHeight());
    245 			if (node.expanded) draw(batch, node.children, indent + indentSpacing);
    246 		}
    247 	}
    248 
    249 	/** @return May be null. */
    250 	public Node getNodeAt (float y) {
    251 		foundNode = null;
    252 		getNodeAt(rootNodes, y, getHeight());
    253 		return foundNode;
    254 	}
    255 
    256 	private float getNodeAt (Array<Node> nodes, float y, float rowY) {
    257 		for (int i = 0, n = nodes.size; i < n; i++) {
    258 			Node node = nodes.get(i);
    259 			if (y >= rowY - node.height - ySpacing && y < rowY) {
    260 				foundNode = node;
    261 				return -1;
    262 			}
    263 			rowY -= node.height + ySpacing;
    264 			if (node.expanded) {
    265 				rowY = getNodeAt(node.children, y, rowY);
    266 				if (rowY == -1) return -1;
    267 			}
    268 		}
    269 		return rowY;
    270 	}
    271 
    272 	void selectNodes (Array<Node> nodes, float low, float high) {
    273 		for (int i = 0, n = nodes.size; i < n; i++) {
    274 			Node node = nodes.get(i);
    275 			if (node.actor.getY() < low) break;
    276 			if (!node.isSelectable()) continue;
    277 			if (node.actor.getY() <= high) selection.add(node);
    278 			if (node.expanded) selectNodes(node.children, low, high);
    279 		}
    280 	}
    281 
    282 	public Selection<Node> getSelection () {
    283 		return selection;
    284 	}
    285 
    286 	public TreeStyle getStyle () {
    287 		return style;
    288 	}
    289 
    290 	public Array<Node> getRootNodes () {
    291 		return rootNodes;
    292 	}
    293 
    294 	public Node getOverNode () {
    295 		return overNode;
    296 	}
    297 
    298 	public void setOverNode (Node overNode) {
    299 		this.overNode = overNode;
    300 	}
    301 
    302 	/** Sets the amount of horizontal space between the nodes and the left/right edges of the tree. */
    303 	public void setPadding (float padding) {
    304 		this.padding = padding;
    305 	}
    306 
    307 	/** Returns the amount of horizontal space for indentation level. */
    308 	public float getIndentSpacing () {
    309 		return indentSpacing;
    310 	}
    311 
    312 	/** Sets the amount of vertical space between nodes. */
    313 	public void setYSpacing (float ySpacing) {
    314 		this.ySpacing = ySpacing;
    315 	}
    316 
    317 	public float getYSpacing () {
    318 		return ySpacing;
    319 	}
    320 
    321 	/** Sets the amount of horizontal space between the node actors and icons. */
    322 	public void setIconSpacing (float left, float right) {
    323 		this.iconSpacingLeft = left;
    324 		this.iconSpacingRight = right;
    325 	}
    326 
    327 	public float getPrefWidth () {
    328 		if (sizeInvalid) computeSize();
    329 		return prefWidth;
    330 	}
    331 
    332 	public float getPrefHeight () {
    333 		if (sizeInvalid) computeSize();
    334 		return prefHeight;
    335 	}
    336 
    337 	public void findExpandedObjects (Array objects) {
    338 		findExpandedObjects(rootNodes, objects);
    339 	}
    340 
    341 	public void restoreExpandedObjects (Array objects) {
    342 		for (int i = 0, n = objects.size; i < n; i++) {
    343 			Node node = findNode(objects.get(i));
    344 			if (node != null) {
    345 				node.setExpanded(true);
    346 				node.expandTo();
    347 			}
    348 		}
    349 	}
    350 
    351 	static boolean findExpandedObjects (Array<Node> nodes, Array objects) {
    352 		boolean expanded = false;
    353 		for (int i = 0, n = nodes.size; i < n; i++) {
    354 			Node node = nodes.get(i);
    355 			if (node.expanded && !findExpandedObjects(node.children, objects)) objects.add(node.object);
    356 		}
    357 		return expanded;
    358 	}
    359 
    360 	/** Returns the node with the specified object, or null. */
    361 	public Node findNode (Object object) {
    362 		if (object == null) throw new IllegalArgumentException("object cannot be null.");
    363 		return findNode(rootNodes, object);
    364 	}
    365 
    366 	static Node findNode (Array<Node> nodes, Object object) {
    367 		for (int i = 0, n = nodes.size; i < n; i++) {
    368 			Node node = nodes.get(i);
    369 			if (object.equals(node.object)) return node;
    370 		}
    371 		for (int i = 0, n = nodes.size; i < n; i++) {
    372 			Node node = nodes.get(i);
    373 			Node found = findNode(node.children, object);
    374 			if (found != null) return found;
    375 		}
    376 		return null;
    377 	}
    378 
    379 	public void collapseAll () {
    380 		collapseAll(rootNodes);
    381 	}
    382 
    383 	static void collapseAll (Array<Node> nodes) {
    384 		for (int i = 0, n = nodes.size; i < n; i++) {
    385 			Node node = nodes.get(i);
    386 			node.setExpanded(false);
    387 			collapseAll(node.children);
    388 		}
    389 	}
    390 
    391 	public void expandAll () {
    392 		expandAll(rootNodes);
    393 	}
    394 
    395 	static void expandAll (Array<Node> nodes) {
    396 		for (int i = 0, n = nodes.size; i < n; i++)
    397 			nodes.get(i).expandAll();
    398 	}
    399 
    400 	/** Returns the click listener the tree uses for clicking on nodes and the over node. */
    401 	public ClickListener getClickListener () {
    402 		return clickListener;
    403 	}
    404 
    405 	static public class Node {
    406 		Actor actor;
    407 		Node parent;
    408 		final Array<Node> children = new Array(0);
    409 		boolean selectable = true;
    410 		boolean expanded;
    411 		Drawable icon;
    412 		float height;
    413 		Object object;
    414 
    415 		public Node (Actor actor) {
    416 			if (actor == null) throw new IllegalArgumentException("actor cannot be null.");
    417 			this.actor = actor;
    418 		}
    419 
    420 		public void setExpanded (boolean expanded) {
    421 			if (expanded == this.expanded) return;
    422 			this.expanded = expanded;
    423 			if (children.size == 0) return;
    424 			Tree tree = getTree();
    425 			if (tree == null) return;
    426 			if (expanded) {
    427 				for (int i = 0, n = children.size; i < n; i++)
    428 					children.get(i).addToTree(tree);
    429 			} else {
    430 				for (int i = 0, n = children.size; i < n; i++)
    431 					children.get(i).removeFromTree(tree);
    432 			}
    433 			tree.invalidateHierarchy();
    434 		}
    435 
    436 		/** Called to add the actor to the tree when the node's parent is expanded. */
    437 		protected void addToTree (Tree tree) {
    438 			tree.addActor(actor);
    439 			if (!expanded) return;
    440 			for (int i = 0, n = children.size; i < n; i++)
    441 				children.get(i).addToTree(tree);
    442 		}
    443 
    444 		/** Called to remove the actor from the tree when the node's parent is collapsed. */
    445 		protected void removeFromTree (Tree tree) {
    446 			tree.removeActor(actor);
    447 			if (!expanded) return;
    448 			for (int i = 0, n = children.size; i < n; i++)
    449 				children.get(i).removeFromTree(tree);
    450 		}
    451 
    452 		public void add (Node node) {
    453 			insert(children.size, node);
    454 		}
    455 
    456 		public void addAll (Array<Node> nodes) {
    457 			for (int i = 0, n = nodes.size; i < n; i++)
    458 				insert(children.size, nodes.get(i));
    459 		}
    460 
    461 		public void insert (int index, Node node) {
    462 			node.parent = this;
    463 			children.insert(index, node);
    464 			updateChildren();
    465 		}
    466 
    467 		public void remove () {
    468 			Tree tree = getTree();
    469 			if (tree != null)
    470 				tree.remove(this);
    471 			else if (parent != null) //
    472 				parent.remove(this);
    473 		}
    474 
    475 		public void remove (Node node) {
    476 			children.removeValue(node, true);
    477 			if (!expanded) return;
    478 			Tree tree = getTree();
    479 			if (tree == null) return;
    480 			node.removeFromTree(tree);
    481 			if (children.size == 0) expanded = false;
    482 		}
    483 
    484 		public void removeAll () {
    485 			Tree tree = getTree();
    486 			if (tree != null) {
    487 				for (int i = 0, n = children.size; i < n; i++)
    488 					children.get(i).removeFromTree(tree);
    489 			}
    490 			children.clear();
    491 		}
    492 
    493 		/** Returns the tree this node is currently in, or null. */
    494 		public Tree getTree () {
    495 			Group parent = actor.getParent();
    496 			if (!(parent instanceof Tree)) return null;
    497 			return (Tree)parent;
    498 		}
    499 
    500 		public Actor getActor () {
    501 			return actor;
    502 		}
    503 
    504 		public boolean isExpanded () {
    505 			return expanded;
    506 		}
    507 
    508 		/** If the children order is changed, {@link #updateChildren()} must be called. */
    509 		public Array<Node> getChildren () {
    510 			return children;
    511 		}
    512 
    513 		public void updateChildren () {
    514 			if (!expanded) return;
    515 			Tree tree = getTree();
    516 			if (tree == null) return;
    517 			for (int i = 0, n = children.size; i < n; i++)
    518 				children.get(i).addToTree(tree);
    519 		}
    520 
    521 		/** @return May be null. */
    522 		public Node getParent () {
    523 			return parent;
    524 		}
    525 
    526 		/** Sets an icon that will be drawn to the left of the actor. */
    527 		public void setIcon (Drawable icon) {
    528 			this.icon = icon;
    529 		}
    530 
    531 		public Object getObject () {
    532 			return object;
    533 		}
    534 
    535 		/** Sets an application specific object for this node. */
    536 		public void setObject (Object object) {
    537 			this.object = object;
    538 		}
    539 
    540 		public Drawable getIcon () {
    541 			return icon;
    542 		}
    543 
    544 		public int getLevel () {
    545 			int level = 0;
    546 			Node current = this;
    547 			do {
    548 				level++;
    549 				current = current.getParent();
    550 			} while (current != null);
    551 			return level;
    552 		}
    553 
    554 		/** Returns this node or the child node with the specified object, or null. */
    555 		public Node findNode (Object object) {
    556 			if (object == null) throw new IllegalArgumentException("object cannot be null.");
    557 			if (object.equals(this.object)) return this;
    558 			return Tree.findNode(children, object);
    559 		}
    560 
    561 		/** Collapses all nodes under and including this node. */
    562 		public void collapseAll () {
    563 			setExpanded(false);
    564 			Tree.collapseAll(children);
    565 		}
    566 
    567 		/** Expands all nodes under and including this node. */
    568 		public void expandAll () {
    569 			setExpanded(true);
    570 			if (children.size > 0) Tree.expandAll(children);
    571 		}
    572 
    573 		/** Expands all parent nodes of this node. */
    574 		public void expandTo () {
    575 			Node node = parent;
    576 			while (node != null) {
    577 				node.setExpanded(true);
    578 				node = node.parent;
    579 			}
    580 		}
    581 
    582 		public boolean isSelectable () {
    583 			return selectable;
    584 		}
    585 
    586 		public void setSelectable (boolean selectable) {
    587 			this.selectable = selectable;
    588 		}
    589 
    590 		public void findExpandedObjects (Array objects) {
    591 			if (expanded && !Tree.findExpandedObjects(children, objects)) objects.add(object);
    592 		}
    593 
    594 		public void restoreExpandedObjects (Array objects) {
    595 			for (int i = 0, n = objects.size; i < n; i++) {
    596 				Node node = findNode(objects.get(i));
    597 				if (node != null) {
    598 					node.setExpanded(true);
    599 					node.expandTo();
    600 				}
    601 			}
    602 		}
    603 	}
    604 
    605 	/** The style for a {@link Tree}.
    606 	 * @author Nathan Sweet */
    607 	static public class TreeStyle {
    608 		public Drawable plus, minus;
    609 		/** Optional. */
    610 		public Drawable over, selection, background;
    611 
    612 		public TreeStyle () {
    613 		}
    614 
    615 		public TreeStyle (Drawable plus, Drawable minus, Drawable selection) {
    616 			this.plus = plus;
    617 			this.minus = minus;
    618 			this.selection = selection;
    619 		}
    620 
    621 		public TreeStyle (TreeStyle style) {
    622 			this.plus = style.plus;
    623 			this.minus = style.minus;
    624 			this.selection = style.selection;
    625 		}
    626 	}
    627 }
    628