1 /* 2 * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 8 * - Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 11 * - Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 15 * - Neither the name of Oracle nor the names of its 16 * contributors may be used to endorse or promote products derived 17 * from this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 20 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 24 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 /* 33 * This source code is provided to illustrate the usage of a given feature 34 * or technique and has been deliberately simplified. Additional steps 35 * required for a production-quality application, such as security checks, 36 * input validation and proper error handling, might not be present in 37 * this sample code. 38 */ 39 40 41 42 import java.lang.reflect.InvocationTargetException; 43 import java.util.logging.Level; 44 import java.util.logging.Logger; 45 import javax.swing.*; 46 import javax.swing.event.*; 47 import java.awt.BorderLayout; 48 import java.awt.Color; 49 import java.awt.Dimension; 50 import java.awt.FlowLayout; 51 import java.awt.event.ActionEvent; 52 import java.awt.event.ActionListener; 53 import java.util.*; 54 import javax.swing.UIManager.LookAndFeelInfo; 55 import javax.swing.border.*; 56 import javax.swing.tree.*; 57 58 59 /** 60 * A demo for illustrating how to do different things with JTree. 61 * The data that this displays is rather boring, that is each node will 62 * have 7 children that have random names based on the fonts. Each node 63 * is then drawn with that font and in a different color. 64 * While the data isn't interesting the example illustrates a number 65 * of things: 66 * 67 * For an example of dynamicaly loading children refer to DynamicTreeNode. 68 * For an example of adding/removing/inserting/reloading refer to the inner 69 * classes of this class, AddAction, RemovAction, InsertAction and 70 * ReloadAction. 71 * For an example of creating your own cell renderer refer to 72 * SampleTreeCellRenderer. 73 * For an example of subclassing JTreeModel for editing refer to 74 * SampleTreeModel. 75 * 76 * @author Scott Violet 77 */ 78 public final class SampleTree { 79 80 /** Window for showing Tree. */ 81 protected JFrame frame; 82 /** Tree used for the example. */ 83 protected JTree tree; 84 /** Tree model. */ 85 protected DefaultTreeModel treeModel; 86 87 /** 88 * Constructs a new instance of SampleTree. 89 */ 90 public SampleTree() { 91 // Trying to set Nimbus look and feel 92 try { 93 for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { 94 if ("Nimbus".equals(info.getName())) { 95 UIManager.setLookAndFeel(info.getClassName()); 96 break; 97 } 98 } 99 } catch (Exception ignored) { 100 } 101 102 JMenuBar menuBar = constructMenuBar(); 103 JPanel panel = new JPanel(true); 104 105 frame = new JFrame("SampleTree"); 106 frame.getContentPane().add("Center", panel); 107 frame.setJMenuBar(menuBar); 108 frame.setBackground(Color.lightGray); 109 110 /* Create the JTreeModel. */ 111 DefaultMutableTreeNode root = createNewNode("Root"); 112 treeModel = new SampleTreeModel(root); 113 114 /* Create the tree. */ 115 tree = new JTree(treeModel); 116 117 /* Enable tool tips for the tree, without this tool tips will not 118 be picked up. */ 119 ToolTipManager.sharedInstance().registerComponent(tree); 120 121 /* Make the tree use an instance of SampleTreeCellRenderer for 122 drawing. */ 123 tree.setCellRenderer(new SampleTreeCellRenderer()); 124 125 /* Make tree ask for the height of each row. */ 126 tree.setRowHeight(-1); 127 128 /* Put the Tree in a scroller. */ 129 JScrollPane sp = new JScrollPane(); 130 sp.setPreferredSize(new Dimension(300, 300)); 131 sp.getViewport().add(tree); 132 133 /* And show it. */ 134 panel.setLayout(new BorderLayout()); 135 panel.add("Center", sp); 136 panel.add("South", constructOptionsPanel()); 137 138 frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 139 frame.pack(); 140 frame.setVisible(true); 141 } 142 143 /** Constructs a JPanel containing check boxes for the different 144 * options that tree supports. */ 145 @SuppressWarnings("serial") 146 private JPanel constructOptionsPanel() { 147 JCheckBox aCheckbox; 148 JPanel retPanel = new JPanel(false); 149 JPanel borderPane = new JPanel(false); 150 151 borderPane.setLayout(new BorderLayout()); 152 retPanel.setLayout(new FlowLayout()); 153 154 aCheckbox = new JCheckBox("show top level handles"); 155 aCheckbox.setSelected(tree.getShowsRootHandles()); 156 aCheckbox.addChangeListener(new ShowHandlesChangeListener()); 157 retPanel.add(aCheckbox); 158 159 aCheckbox = new JCheckBox("show root"); 160 aCheckbox.setSelected(tree.isRootVisible()); 161 aCheckbox.addChangeListener(new ShowRootChangeListener()); 162 retPanel.add(aCheckbox); 163 164 aCheckbox = new JCheckBox("editable"); 165 aCheckbox.setSelected(tree.isEditable()); 166 aCheckbox.addChangeListener(new TreeEditableChangeListener()); 167 aCheckbox.setToolTipText("Triple click to edit"); 168 retPanel.add(aCheckbox); 169 170 borderPane.add(retPanel, BorderLayout.CENTER); 171 172 /* Create a set of radio buttons that dictate what selection should 173 be allowed in the tree. */ 174 ButtonGroup group = new ButtonGroup(); 175 JPanel buttonPane = new JPanel(false); 176 JRadioButton button; 177 178 buttonPane.setLayout(new FlowLayout()); 179 buttonPane.setBorder(new TitledBorder("Selection Mode")); 180 button = new JRadioButton("Single"); 181 button.addActionListener(new AbstractAction() { 182 183 @Override 184 public boolean isEnabled() { 185 return true; 186 } 187 188 public void actionPerformed(ActionEvent e) { 189 tree.getSelectionModel().setSelectionMode( 190 TreeSelectionModel.SINGLE_TREE_SELECTION); 191 } 192 }); 193 group.add(button); 194 buttonPane.add(button); 195 button = new JRadioButton("Contiguous"); 196 button.addActionListener(new AbstractAction() { 197 198 @Override 199 public boolean isEnabled() { 200 return true; 201 } 202 203 public void actionPerformed(ActionEvent e) { 204 tree.getSelectionModel().setSelectionMode( 205 TreeSelectionModel.CONTIGUOUS_TREE_SELECTION); 206 } 207 }); 208 group.add(button); 209 buttonPane.add(button); 210 button = new JRadioButton("Discontiguous"); 211 button.addActionListener(new AbstractAction() { 212 213 @Override 214 public boolean isEnabled() { 215 return true; 216 } 217 218 public void actionPerformed(ActionEvent e) { 219 tree.getSelectionModel().setSelectionMode( 220 TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); 221 } 222 }); 223 button.setSelected(true); 224 group.add(button); 225 buttonPane.add(button); 226 227 borderPane.add(buttonPane, BorderLayout.SOUTH); 228 229 // NOTE: This will be enabled in a future release. 230 // Create a label and combobox to determine how many clicks are 231 // needed to expand. 232 /* 233 JPanel clickPanel = new JPanel(); 234 Object[] values = { "Never", new Integer(1), 235 new Integer(2), new Integer(3) }; 236 final JComboBox clickCBox = new JComboBox(values); 237 238 clickPanel.setLayout(new FlowLayout()); 239 clickPanel.add(new JLabel("Click count to expand:")); 240 clickCBox.setSelectedIndex(2); 241 clickCBox.addActionListener(new ActionListener() { 242 public void actionPerformed(ActionEvent ae) { 243 Object selItem = clickCBox.getSelectedItem(); 244 245 if(selItem instanceof Integer) 246 tree.setToggleClickCount(((Integer)selItem).intValue()); 247 else // Don't toggle 248 tree.setToggleClickCount(0); 249 } 250 }); 251 clickPanel.add(clickCBox); 252 borderPane.add(clickPanel, BorderLayout.NORTH); 253 */ 254 return borderPane; 255 } 256 257 /** Construct a menu. */ 258 private JMenuBar constructMenuBar() { 259 JMenu menu; 260 JMenuBar menuBar = new JMenuBar(); 261 JMenuItem menuItem; 262 263 /* Good ol exit. */ 264 menu = new JMenu("File"); 265 menuBar.add(menu); 266 267 menuItem = menu.add(new JMenuItem("Exit")); 268 menuItem.addActionListener(new ActionListener() { 269 270 public void actionPerformed(ActionEvent e) { 271 System.exit(0); 272 } 273 }); 274 275 /* Tree related stuff. */ 276 menu = new JMenu("Tree"); 277 menuBar.add(menu); 278 279 menuItem = menu.add(new JMenuItem("Add")); 280 menuItem.addActionListener(new AddAction()); 281 282 menuItem = menu.add(new JMenuItem("Insert")); 283 menuItem.addActionListener(new InsertAction()); 284 285 menuItem = menu.add(new JMenuItem("Reload")); 286 menuItem.addActionListener(new ReloadAction()); 287 288 menuItem = menu.add(new JMenuItem("Remove")); 289 menuItem.addActionListener(new RemoveAction()); 290 291 return menuBar; 292 } 293 294 /** 295 * Returns the TreeNode instance that is selected in the tree. 296 * If nothing is selected, null is returned. 297 */ 298 protected DefaultMutableTreeNode getSelectedNode() { 299 TreePath selPath = tree.getSelectionPath(); 300 301 if (selPath != null) { 302 return (DefaultMutableTreeNode) selPath.getLastPathComponent(); 303 } 304 return null; 305 } 306 307 /** 308 * Returns the selected TreePaths in the tree, may return null if 309 * nothing is selected. 310 */ 311 protected TreePath[] getSelectedPaths() { 312 return tree.getSelectionPaths(); 313 } 314 315 protected DefaultMutableTreeNode createNewNode(String name) { 316 return new DynamicTreeNode(new SampleData(null, Color.black, name)); 317 } 318 319 320 /** 321 * AddAction is used to add a new item after the selected item. 322 */ 323 class AddAction extends Object implements ActionListener { 324 325 /** Number of nodes that have been added. */ 326 public int addCount; 327 328 /** 329 * Messaged when the user clicks on the Add menu item. 330 * Determines the selection from the Tree and adds an item 331 * after that. If nothing is selected, an item is added to 332 * the root. 333 */ 334 public void actionPerformed(ActionEvent e) { 335 DefaultMutableTreeNode lastItem = getSelectedNode(); 336 DefaultMutableTreeNode parent; 337 338 /* Determine where to create the new node. */ 339 if (lastItem != null) { 340 parent = (DefaultMutableTreeNode) lastItem.getParent(); 341 if (parent == null) { 342 parent = (DefaultMutableTreeNode) treeModel.getRoot(); 343 lastItem = null; 344 } 345 } else { 346 parent = (DefaultMutableTreeNode) treeModel.getRoot(); 347 } 348 if (parent == null) { 349 // new root 350 treeModel.setRoot(createNewNode("Added " + Integer.toString( 351 addCount++))); 352 } else { 353 int newIndex; 354 if (lastItem == null) { 355 newIndex = treeModel.getChildCount(parent); 356 } else { 357 newIndex = parent.getIndex(lastItem) + 1; 358 } 359 360 /* Let the treemodel know. */ 361 treeModel.insertNodeInto(createNewNode("Added " + Integer. 362 toString(addCount++)), 363 parent, newIndex); 364 } 365 } 366 } // End of SampleTree.AddAction 367 368 369 /** 370 * InsertAction is used to insert a new item before the selected item. 371 */ 372 class InsertAction extends Object implements ActionListener { 373 374 /** Number of nodes that have been added. */ 375 public int insertCount; 376 377 /** 378 * Messaged when the user clicks on the Insert menu item. 379 * Determines the selection from the Tree and inserts an item 380 * after that. If nothing is selected, an item is added to 381 * the root. 382 */ 383 public void actionPerformed(ActionEvent e) { 384 DefaultMutableTreeNode lastItem = getSelectedNode(); 385 DefaultMutableTreeNode parent; 386 387 /* Determine where to create the new node. */ 388 if (lastItem != null) { 389 parent = (DefaultMutableTreeNode) lastItem.getParent(); 390 if (parent == null) { 391 parent = (DefaultMutableTreeNode) treeModel.getRoot(); 392 lastItem = null; 393 } 394 } else { 395 parent = (DefaultMutableTreeNode) treeModel.getRoot(); 396 } 397 if (parent == null) { 398 // new root 399 treeModel.setRoot(createNewNode("Inserted " + Integer.toString( 400 insertCount++))); 401 } else { 402 int newIndex; 403 404 if (lastItem == null) { 405 newIndex = treeModel.getChildCount(parent); 406 } else { 407 newIndex = parent.getIndex(lastItem); 408 } 409 410 /* Let the treemodel know. */ 411 treeModel.insertNodeInto(createNewNode("Inserted " + Integer. 412 toString(insertCount++)), 413 parent, newIndex); 414 } 415 } 416 } // End of SampleTree.InsertAction 417 418 419 /** 420 * ReloadAction is used to reload from the selected node. If nothing 421 * is selected, reload is not issued. 422 */ 423 class ReloadAction extends Object implements ActionListener { 424 425 /** 426 * Messaged when the user clicks on the Reload menu item. 427 * Determines the selection from the Tree and asks the treemodel 428 * to reload from that node. 429 */ 430 public void actionPerformed(ActionEvent e) { 431 DefaultMutableTreeNode lastItem = getSelectedNode(); 432 433 if (lastItem != null) { 434 treeModel.reload(lastItem); 435 } 436 } 437 } // End of SampleTree.ReloadAction 438 439 440 /** 441 * RemoveAction removes the selected node from the tree. If 442 * The root or nothing is selected nothing is removed. 443 */ 444 class RemoveAction extends Object implements ActionListener { 445 446 /** 447 * Removes the selected item as long as it isn't root. 448 */ 449 public void actionPerformed(ActionEvent e) { 450 TreePath[] selected = getSelectedPaths(); 451 452 if (selected != null && selected.length > 0) { 453 TreePath shallowest; 454 455 // The remove process consists of the following steps: 456 // 1 - find the shallowest selected TreePath, the shallowest 457 // path is the path with the smallest number of path 458 // components. 459 // 2 - Find the siblings of this TreePath 460 // 3 - Remove from selected the TreePaths that are descendants 461 // of the paths that are going to be removed. They will 462 // be removed as a result of their ancestors being 463 // removed. 464 // 4 - continue until selected contains only null paths. 465 while ((shallowest = findShallowestPath(selected)) != null) { 466 removeSiblings(shallowest, selected); 467 } 468 } 469 } 470 471 /** 472 * Removes the sibling TreePaths of <code>path</code>, that are 473 * located in <code>paths</code>. 474 */ 475 private void removeSiblings(TreePath path, TreePath[] paths) { 476 // Find the siblings 477 if (path.getPathCount() == 1) { 478 // Special case, set the root to null 479 for (int counter = paths.length - 1; counter >= 0; counter--) { 480 paths[counter] = null; 481 } 482 treeModel.setRoot(null); 483 } else { 484 // Find the siblings of path. 485 TreePath parent = path.getParentPath(); 486 MutableTreeNode parentNode = (MutableTreeNode) parent. 487 getLastPathComponent(); 488 ArrayList<TreePath> toRemove = new ArrayList<TreePath>(); 489 490 // First pass, find paths with a parent TreePath of parent 491 for (int counter = paths.length - 1; counter >= 0; counter--) { 492 if (paths[counter] != null && paths[counter].getParentPath(). 493 equals(parent)) { 494 toRemove.add(paths[counter]); 495 paths[counter] = null; 496 } 497 } 498 499 // Second pass, remove any paths that are descendants of the 500 // paths that are going to be removed. These paths are 501 // implicitly removed as a result of removing the paths in 502 // toRemove 503 int rCount = toRemove.size(); 504 for (int counter = paths.length - 1; counter >= 0; counter--) { 505 if (paths[counter] != null) { 506 for (int rCounter = rCount - 1; rCounter >= 0; 507 rCounter--) { 508 if ((toRemove.get(rCounter)).isDescendant( 509 paths[counter])) { 510 paths[counter] = null; 511 } 512 } 513 } 514 } 515 516 // Sort the siblings based on position in the model 517 if (rCount > 1) { 518 Collections.sort(toRemove, new PositionComparator()); 519 } 520 int[] indices = new int[rCount]; 521 Object[] removedNodes = new Object[rCount]; 522 for (int counter = rCount - 1; counter >= 0; counter--) { 523 removedNodes[counter] = (toRemove.get(counter)). 524 getLastPathComponent(); 525 indices[counter] = treeModel.getIndexOfChild(parentNode, 526 removedNodes[counter]); 527 parentNode.remove(indices[counter]); 528 } 529 treeModel.nodesWereRemoved(parentNode, indices, removedNodes); 530 } 531 } 532 533 /** 534 * Returns the TreePath with the smallest path count in 535 * <code>paths</code>. Will return null if there is no non-null 536 * TreePath is <code>paths</code>. 537 */ 538 private TreePath findShallowestPath(TreePath[] paths) { 539 int shallowest = -1; 540 TreePath shallowestPath = null; 541 542 for (int counter = paths.length - 1; counter >= 0; counter--) { 543 if (paths[counter] != null) { 544 if (shallowest != -1) { 545 if (paths[counter].getPathCount() < shallowest) { 546 shallowest = paths[counter].getPathCount(); 547 shallowestPath = paths[counter]; 548 if (shallowest == 1) { 549 return shallowestPath; 550 } 551 } 552 } else { 553 shallowestPath = paths[counter]; 554 shallowest = paths[counter].getPathCount(); 555 } 556 } 557 } 558 return shallowestPath; 559 } 560 561 562 /** 563 * An Comparator that bases the return value on the index of the 564 * passed in objects in the TreeModel. 565 * <p> 566 * This is actually rather expensive, it would be more efficient 567 * to extract the indices and then do the comparision. 568 */ 569 private class PositionComparator implements Comparator<TreePath> { 570 571 public int compare(TreePath p1, TreePath p2) { 572 int p1Index = treeModel.getIndexOfChild(p1.getParentPath(). 573 getLastPathComponent(), p1.getLastPathComponent()); 574 int p2Index = treeModel.getIndexOfChild(p2.getParentPath(). 575 getLastPathComponent(), p2.getLastPathComponent()); 576 return p1Index - p2Index; 577 } 578 } 579 } // End of SampleTree.RemoveAction 580 581 582 /** 583 * ShowHandlesChangeListener implements the ChangeListener interface 584 * to toggle the state of showing the handles in the tree. 585 */ 586 class ShowHandlesChangeListener extends Object implements ChangeListener { 587 588 public void stateChanged(ChangeEvent e) { 589 tree.setShowsRootHandles(((JCheckBox) e.getSource()).isSelected()); 590 } 591 } // End of class SampleTree.ShowHandlesChangeListener 592 593 594 /** 595 * ShowRootChangeListener implements the ChangeListener interface 596 * to toggle the state of showing the root node in the tree. 597 */ 598 class ShowRootChangeListener extends Object implements ChangeListener { 599 600 public void stateChanged(ChangeEvent e) { 601 tree.setRootVisible(((JCheckBox) e.getSource()).isSelected()); 602 } 603 } // End of class SampleTree.ShowRootChangeListener 604 605 606 /** 607 * TreeEditableChangeListener implements the ChangeListener interface 608 * to toggle between allowing editing and now allowing editing in 609 * the tree. 610 */ 611 class TreeEditableChangeListener extends Object implements ChangeListener { 612 613 public void stateChanged(ChangeEvent e) { 614 tree.setEditable(((JCheckBox) e.getSource()).isSelected()); 615 } 616 } // End of class SampleTree.TreeEditableChangeListener 617 618 public static void main(String[] args) { 619 try { 620 SwingUtilities.invokeAndWait(new Runnable() { 621 622 @SuppressWarnings(value = "ResultOfObjectAllocationIgnored") 623 public void run() { 624 new SampleTree(); 625 } 626 }); 627 } catch (InterruptedException ex) { 628 Logger.getLogger(SampleTree.class.getName()).log(Level.SEVERE, null, 629 ex); 630 } catch (InvocationTargetException ex) { 631 Logger.getLogger(SampleTree.class.getName()).log(Level.SEVERE, null, 632 ex); 633 } 634 } 635 }