Trees
Trees are used to display hierarchical information. In this chapter, you learn how to use the SWT Tree control to display and edit a hierarchy of items. When the user clicks the tree, events may occur. Event handling of trees is introduced. Additionally, you learn how to use TreeViewers to program with trees using the MVC approach.
Using Trees
A tree is represented by the org.eclipse.swt.widgets.Tree class in SWT. It may have a hierarchy of items, which are represented by the org.eclipse.swt.widgets.TreeItem class. After a Tree is created, you can add a tree item to it by creating TableItem instances with the tree itself or with one of the tree items in the tree as the tree item's parent.
Creating a Tree
The only constructor of the Tree class is as follows:
public Tree(Composite parent, int style)
You specify the parent composite in the first argument and styles of the tree in the second argument.
Possible styles of trees are:
- SWT.SINGLE, SWT.MULTI: By default, at most one item in the tree is allowed to be selected at any time. If SWT.MULTI is set, more than one item can be selected at the same time. At most one of them should be specified.
- SWT.CHECK: If this style is set, a checkbox is displayed in the front of each tree item. You can use the getChecked method of the TreeItem class to check whether the checkbox associated with this item is checked. You probably want to use this style if you need to perform operations on multiple items "selected" by the user.
In this chapter, you create a simple file browser, as shown in Figure 11-1.
Figure 11-1
The main component in the UI is the tree used to display files. The following is the code to create the tree:
Display display = new Display(); Shell shell = new Shell(display); shell.setLayout(new GridLayout()); Tree tree = new Tree(shell, SWT.BORDER);
Using TreeItems
Tree items are fundamental UI elements to build a tree. You can use any of the constructors of the TreeItem class to create a tree item and add it to a tree.
TreeItem(Tree parent, int style) TreeItem(Tree parent, int style, int index) TreeItem(TreeItem parentItem, int style) TreeItem(TreeItem parentItem, int style, int index)
The first two constructors create top-level tree items in a tree, and the last two constructors create tree items at a deeper level with other tree items as their parents. Because TreeItem does not support any style yet, you can simply put SWT.NULL in the style argument. In the preceding code, the first constructor and the third one append a tree item to the item list, whereas the second one and the last one allow you to specify the index at which a tree item should be stored in the item list.
In the file browser application, the following code is used to add tree items to the tree:
setRootDir(new File("F:/jdk1.5")); /** * Sets the root directory to browse from. * * @param root */ private void setRootDir(File root) { // validates the root first. if( (!root.isDirectory()) || (!root.exists())) throw new IllegalArgumentException("Invalid root: " + root); this.rootDir = root; shell.setText("Now browsing: " + root.getAbsolutePath()); // Remove all the existing items in the tree. if (tree.getItemCount() > 0) { TreeItem[] items = tree.getItems(); for (int i = 0; i < items.length; i++) { items[i].dispose(); // Dispose itself and all of its descendants. } } // Adds files under the root to the tree. File[] files = root.listFiles(); for(int i=0; files != null && i < files.length; i++) addFileToTree(tree, files[i]); } /** * Wraps the file in a TreeItem and adds the TreeItem to the Tree. * * @param parent * @param file */ private void addFileToTree(Object parent, File file) { TreeItem item = null; if (parent instanceof Tree) item = new TreeItem((Tree) parent, SWT.NULL); else if (parent instanceof TreeItem) item = new TreeItem((TreeItem) parent, SWT.NULL); else throw new IllegalArgumentException( "parent should be a tree or a tree item: " + parent); item.setText(file.getName()); item.setImage(getIcon(file)); item.setData(file); if (file.isDirectory()) { if (file.list() != null && file.list().length > 0) new TreeItem(item, SWT.NULL); } }
When the setRootDir method is called, it first disposes of all the existing tree items in the tree and calls the addFileToTree method. The addFileToTree method creates a TreeItem representing the file passed. The setText and setImage methods are used to set the text label and the image label for the TreeItem. You also use the setData method to record the file object in the tree item. If the file is a directory and has one or more files under it, a dummy tree item is created for it to display a plus sign in front of the tree item so that the user can click it to browse files under it. When the user tries to expand the tree item, files under the directory are listed and represented using tree items (more details are in the section "Handling Events"). You can recursively build the whole tree completely in one call. However, this operation is very time-consuming. Therefore, we use this optimized approach instead.
In the preceding setRootDir method, the dispose method of the TreeItem class is used to dispose of tree items. As a result, tree items are removed from the tree.
The following are some frequently used methods in the TreeItem class. To get the parent of a tree item, you can use the getParent method or the getParentItem method:
public Tree getParent() public TreeItem getParentItem()
If the tree item is a top-level item, the first method returns the tree control and the second method returns null. Otherwise, the first method returns the tree control containing the tree item and the second method returns the parent item of the tree item.
The getItems method returns all the direct children of a tree item:
public TreeItem[] getItems()
To get the total number of a tree item's direct children, use the getItemCount method:
public int getItemCount()
To get and set the expand status of a tree item, use the following methods:
public boolean getExpanded() public void setExpanded(boolean expanded)
Similarly, you can use these two methods to get and set the check status of a tree item (only applicable to tree items in tables with the SWT.CHECK style):
public boolean getChecked() public void setChecked(boolean checked)
The Tree class provides a few methods to access tree items that it contains.
The following methods return information about top-level tree items in a tree:
public int getItemCount() public TreeItem[] getItems()
To get and set the top item displayed in a tree, use the following methods:
public TreeItem getTopItem() public void setTopItem(TreeItem item)
Use the removeAll method to remove all the tree items from a tree:
Handling Events
The Tree control generates the following events in addition to events inherited from the Composite class: SWT.Selection, SWT.DefaultSelection, SWT.Collapse, and SWT.Expand. A SelectionListener is able to handle SWT.Selection and SWT.DefaultSelection events, and a TreeListener listens for SWT.Collapse and SWT.Expand events.
To implement the optimized tree building mechanism described in the preceding section, we need a listener to listen for SWT.Expand events:
tree.addTreeListener(new TreeListener() { public void treeCollapsed(TreeEvent e) { } public void treeExpanded(TreeEvent e) { TreeItem item = (TreeItem) e.item; TreeItem[] children = item.getItems(); for (int i = 0; i < children.length; i++) if (children[i].getData() == null) // Removes dummy items. children[i].dispose(); else // Child files already added to the tree. return; File[] files = ((File) item.getData()).listFiles(); for (int i = 0; files != null && i < files.length; i++) buildTree(item, files[i]); } });
When the user clicks the plus sign in front of a tree item, an SWT.Expand event is generated. As a result, the treeExpanded method in the listener is called. First, the tree item being expanded is obtained. We then check the existing child items of this tree item. If the child items are valid, this means they have been constructed properly already. If the child items are dummy ones, you dispose of them and build a tree item for each file under the directory represented by the tree item being expanded.
To enable the user to launch a file from the tree, you add a selection listener as follows:
tree.addSelectionListener(new SelectionListener() { public void widgetSelected(SelectionEvent e) { } public void widgetDefaultSelected(SelectionEvent e) { TreeItem item = (TreeItem) e.item; File file = (File) item.getData(); if (Program.launch(file.getAbsolutePath())) { System.out.println("File has been launched: " + file); } else { System.out.println("Unable to launch file: " + file); } } });
When the user double-clicks a tree item or presses Enter while a tree item is selected, a default selection event is generated and the widgetDefaultSelected method gets called. This method first retrieves the file object from the selected item and tries to launch it using the launch method of the org.eclipse.swt.program.Program class. For example, when the user default selects the LICENSE.rtf file (see Figure 11-2) on Windows, the system opens the file with Microsoft Word.
Figure 11-2
The Tree class provides some methods for you to get and set selection items.
You can use the following two methods to get information about selected items:
public int getSelectionCount() public TreeItem[] getSelection()
The getSelectionCount method returns the total number of selected items, and the getSelection method returns all the selected items as an array.
You can clear the current selection and set new selection items using the setSelection method:
public void setSelection(TreeItem[] items)
The following methods can be used to select/deselect all the tree items in a tree:
public void selectAll() public void deselectAll()
Using TreeEditors
In Chapter 10, you learned how to use TableEditors to implement the cell editing function. A TreeEditor works almost the same as a TableEditor, except a TreeEditor manages controls for editing items in a tree instead of a table.
In this section, we add the file renaming function to our file browser by using a TreeEditor (see Figure 11-3).
Figure 11-3
First, you create a TreeEditor for the tree:
final TreeEditor editor = new TreeEditor(tree);
Then, you add the following mouse down listener to the tree:
tree.addListener(SWT.MouseDown, new Listener() { public void handleEvent(Event event) { // Locates the File position in the Tree. Point point = new Point(event.x, event.y); final TreeItem item = tree.getItem(point); if (item == null) return; final Text text = new Text(tree, SWT.NONE); text.setText(item.getText()); text.setBackground( shell.getDisplay().getSystemColor(SWT.COLOR_YELLOW)); editor.horizontalAlignment = SWT.LEFT; editor.grabHorizontal = true; editor.setEditor(text, item); Listener textListener = new Listener() { public void handleEvent(final Event e) { switch (e.type) { case SWT.FocusOut : File renamed = renameFile( (File) item.getData(), text.getText()); if (renamed != null) { item.setText(text.getText()); item.setData(renamed); } text.dispose(); break; case SWT.Traverse : switch (e.detail) { case SWT.TRAVERSE_RETURN : renamed = renameFile((File) item.getData(), text.getText()); if (renamed != null) { item.setText(text.getText()); item.setData(renamed); } case SWT.TRAVERSE_ESCAPE : text.dispose(); e.doit = false; } break; } } }; text.addListener(SWT.FocusOut, textListener); text.addListener(SWT.Traverse, textListener); text.setFocus(); } });
First, the tree item at the point that the user clicks is determined using the getItem(Point point) method of the Tree class. Then we create a Text control and set it as the editor using the setEditor method of the TreeEditor class. The properties of the editor are configured (for more details, see Chapter 10).
A listener is added to the text control to listen for SWT.FocusOut and SWT.Traverse events. If the text control loses focus, you try to rename the file with the new name in the text control using the renameFile method.
/**
* Renames the given file to the specified new name.
*
* @param file
* @param newName
* @return the new file name or null
if renaming
* fails.
*/
private File renameFile(File file, String newName) {
File dest = new File(file.getParentFile(), newName);
if (file.renameTo(dest)) {
return dest;
} else {
return null;
}
}
* @param newName
* @return the new file name or null
if renaming
* fails.
*/
private File renameFile(File file, String newName) {
File dest = new File(file.getParentFile(), newName);
if (file.renameTo(dest)) {
return dest;
} else {
return null;
}
}
If the file is successfully renamed, the tree item is updated.
Using TreeViewer
TreeViewer is the MVC viewer that corresponds to the SWT Tree control. Programming a TreeViewer is very similar to programming other viewers such as ListViewers and TableViewers, which you learned about in previous chapters.
The basic steps for programming a TreeViewer are:
- Create domain-specific model objects.
- Create TreeViewers.
- Set content providers and content.
- Set label providers (optional).
- Add selection listeners (optional).
- Add filters (optional).
- Set sorters (optional).
- Handle events (optional).
Note that, unlike a TableViewer, a TreeViewer does not support direct editing. To make tree items editable, you have to use the same technique used in the preceding section (i.e., you have to add a MouseListener to the TreeItem and respond when a user clicks the mouse button).
In the following sections, we are going to rewrite the file browser application using a tree viewer.
Because the File class is the perfect model class and files in the system are the model objects for the file browser, Step 1 is skipped. You go directly go to Step 2 to create a TreeViewer.
Creating a TreeViewer
There are three constructors in the TreeViewer class that you can use to create TreeViewers:
TreeViewer(Composite parent) TreeViewer(Composite parent, int style) TreeViewer(Tree tree)
The first two constructors create a TreeViewer on a new tree control under the given parent composite, while the third constructor creates a TreeViewer based on an existing tree control. Here is the code to create a tree viewer for the file browser application:
TreeViewer treeViewer = new TreeViewer(shell, SWT.BORDER); treeViewer.getTree().setLayoutData(new GridData(GridData.FILL_BOTH));
First the tree viewer is created. The newly created Tree control has the SWT.BORDER style. You then set the layout data for the tree embedded in the tree viewer. The SWT Tree control is obtained through the getTree method:
Setting the Content Provider
After the TreeViewer is created, you then set the content provider for it. A content provider mediates between a viewer's model and the viewer itself. The TreeViewer has its own particular type of content provider: ITreeContentProvider. You use the following code to set the content provider for the tree viewer:
treeViewer.setContentProvider(new ITreeContentProvider() { public Object[] getChildren(Object parentElement) { File[] files = ((File)parentElement).listFiles(); if(files == null) return new Object[0]; return files; } public Object getParent(Object element) { return ((File)element).getParentFile(); } public boolean hasChildren(Object element) { File file = (File)element; File[] files = file.listFiles(); if(files == null || files.length == 0) return false; return true; } public Object[] getElements(Object inputElement) { File[] files = ((File)inputElement).listFiles(); if(files == null) return new Object[0]; return files; } public void dispose() { } public void inputChanged( Viewer viewer, Object oldInput, Object newInput) { shell.setText("Now browsing: " + newInput); } });
Note that the ITreeContentProvider interface defines three additional methods over the IStructuredContentProvider interface: getChildren, getParent, and hasChildren.
The following line sets the input element:
treeViewer.setInput(new File("F:/jdk1.5"));
When the setInput method is executed, the inputChanged method in the content provider is called. The tree viewer queries the getElements of the tree content provider for root elements. While adding root elements to the tree, the tree viewer also consults the hasChildren method to check whether an element has any children. When the user expands a tree item, the getChildren method is used to get all the files under the directory represented by it.
Setting the Label Provider
A label provider maps an element of the viewer's model to an optional image and optional text string used to display the element in the viewer's control. The following is the code to set the label provider for the tree viewer:
treeViewer.setLabelProvider(new LabelProvider() { public Image getImage(Object element) { return getIcon((File)element); } public String getText(Object element) { return ((File)element).getName(); } });
The getImage method returns an icon (image) representing the file type of the file, and the getText method returns the short form of the file name.
Setting the Sorter
To organize the files in the file browser, you need to set a sorter to sort the files, as shown in Figure 11-4.
Figure 11-4
The code to implement the sorter is shown here:
// Sorts the tree in the order that directories go before all normal files treeViewer.setSorter(new ViewerSorter() { public int category(Object element) { File file = (File)element; if(file.isDirectory()) return 0; else return 1; } });
In the sorter, only the category method of the ViewerSorter class is overridden. The category method returns the category that the given element belongs to. There are two categories for files here: directories and normal files. The compare method of the ViewerSorter class compares the elements' categories as computed by the category method. Elements within the same category are further subjected to a case-insensitive compare of their label strings computed by the tree viewer's label provider.
Adding a Filter
In this section, we are going to create a filter to show only the directories, as shown in Figure 11-5.
Figure 11-5
Here is the code to filter out normal files:
final ViewerFilter directoryFilter = new ViewerFilter() { public boolean select( Viewer viewer, Object parentElement, Object element) { return ((File)element).isDirectory(); } }; Action actionShowDirectoriesOnly = new Action("Show directories only") { public void run() { if(! isChecked()) treeViewer.removeFilter(directoryFilter); else treeViewer.addFilter(directoryFilter); } }; actionShowDirectoriesOnly.setChecked(false); ToolBar toolBar = new ToolBar(shell, SWT.FLAT); ToolBarManager manager = new ToolBarManager(toolBar); manager.add(actionSetRootDir); manager.add(actionShowDirectoriesOnly); manager.update(true);
If the "Show directories only" button is checked, the filter is added to the tree viewer. As a result, only directories are shown. If the button is unchecked, the filter is removed from the tree viewer so that all kinds of files are displayed.
Getting Selections
Let's add the file deletion function to the file browser application, as shown in Figure 11-6.
Figure 11-6
When the user presses the "Delete the selected file" button, a message box appears. If the user clicks Yes, the application tries to delete the file. The code to implement this function is as follows:
Action actionDeleteFile = new Action("Delete the selected file") { public void run() { IStructuredSelection selection = (IStructuredSelection)treeViewer.getSelection(); File file = (File)selection.getFirstElement(); if(file == null) { System.out.println("Please select a file first."); return; } MessageBox messageBox = new MessageBox(shell, SWT.YES | SWT.NO); messageBox.setMessage("Are you sure to delete file: " + file.getName() + "?"); if(messageBox.open() == SWT.YES) { File parentFile = file.getParentFile(); if(file.delete()) { System.out.println("File has been deleted. "); // Notifies the viewer for update. treeViewer.refresh(parentFile, false); }else{ System.out.println("Unable to delete file."); } } } };
If the user clicks the deletion button, the run method of the action is invoked. First, the selected element (file) is extracted. If no file is selected, a message is printed out and the function returns. A message box is displayed to ask the user to confirm the deletion. If the file is deleted successfully, you update the tree viewer with the refresh method:
public void refresh(Object element, boolean updateLabels)
This method refreshes the tree viewer starting with the given element.
If you need to listen to selection events generated by the tree, you can simply add an ISelectionChangedListener using the addSelectionChangedListener method of the TreeViewer class:
public void addSelectionChangedListener(ISelectionChangedListener listener)
With the preceding method, you can register selection listeners that will be invoked when a selection in the tree is made.
Summary
In this chapter, you learned about the Tree control. Trees are powerful UI components for displaying hierarchical data. After a tree is created, you can add an item to it by wrapping the item in a TreeItem. Trees are capable of generating selection, collapse, and expand events. You can register proper event listeners to listen to those events. By setting tree editors, you make it possible for the user to edit the data on the fly. To program with trees using the MVC approach, you can use the TreeViewer class provided in the JFace viewer framework.