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