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 }