Chapter 37. Tree Widget

Table of Contents
37.1. Tree Item Widget
37.2. Tree Example

Inheritance Hierarchy

Object
   +--- Widget
         +--- Container
               +--- Tree
         

The purpose of tree widgets is to display hierarchically-organized data. The Tree widget itself is a vertical container for widgets of type TreeItem. Tree itself is not terribly different from CList - both are derived directly from Container, and the Container methods work in the same way on Tree widgets as on CList widgets. The difference is that Tree widgets can be nested within other Tree widgets. We'll see how to do this shortly.

The Tree widget has its own window, and defaults to a white background, as does CList. Also, most of the Tree methods work in the same way as the corresponding CList ones. However, Tree is not derived from CList, so you cannot use them interchangeably.

A Tree is created in the usual way, using:

$tree = new Gtk::Tree();

Like the CList widget, a Tree will simply keep growing as more items are added to it, as well as when subtrees are expanded. For this reason, they are almost always packed into a ScrolledWindow. You might want to use set_usize() on the scrolled window to ensure that it is big enough to see the tree's items, as the default size for ScrolledWindow is quite small.

Now that you have a tree, you'll probably want to add some items to it. The section on the Tree Item Widget below explains the gory details of TreeItem. For now, it'll suffice to create one, using:

$tree = new_with_label Gtk::TreeItem( $label );

You can then add it to the tree using one of the following:

$tree->append( $tree_item );

$tree->prepend( $tree_item );

Note that you must add items to a Tree one at a time - there is no equivalent to the items() functions of a Gtk::CList.

A subtree is created like any other Tree widget. A subtree is added to another tree beneath a tree item, using:

$tree_item->set_subtree( $subtree );

You do not need to call show() on a subtree before or after adding it to a TreeItem. However, you must have added the TreeItem in question to a parent tree before calling set_subtree(). This is because, technically, the parent of the subtree is not the TreeItem which "owns" it, but rather the Tree which holds that TreeItem.

When you add a subtree to a TreeItem, a plus or minus sign appears beside it, which the user can click on to "expand" or "collapse" it, meaning, to show or hide its subtree. TreeItems are collapsed by default. Note that when you collapse a TreeItem, any selected items in its subtree remain selected, which may not be what the user expects.

As with CList, the Tree type has a selection field, and it is possible to control the behaviour of the tree (somewhat) by setting the selection type using:

$tree->set_selection_mode( $mode );

The semantics associated with the various selection modes are described in the section on the CListWidget . As with the CList widget, the 'select_child', 'unselect_child' (not really - see the discussion about Tree signals below for an explanation), and 'selection_changed' signals are emitted when list items are selected or unselected. However, in order to take advantage of these signals, you need to know which Tree widget they will be emitted by, and where to find the list of selected items.

This is a source of potential confusion. The best way to explain this is that though all Tree widgets are created equal, some are more equal than others. All Tree widgets have their own X window, and can therefore receive events such as mouse clicks (if their TreeItems or their children don't catch them first!). However, to make 'single' and 'browse' selection types behave in a sane manner, the list of selected items is specific to the topmost Tree widget in a hierarchy, known as the "root tree".

Thus, accessing the selection field directly in an arbitrary Tree widget is not a good idea unless you know it's the root tree.

Finally, the 'select_child' (and 'unselect_child', in theory) signals are emitted by all trees, but the 'selection_changed' signal is only emitted by the root tree. Consequently, if you want to handle the 'select_child' signal for a tree and all its subtrees, you will have to call signal_connect() for every subtree.

The Fields in the Tree (from the C structure) looks like this:

container
children
root_tree
tree_owner
selection
level
indent_value
current_index
selection_mode
view_mode
view_line

In addition, is_root_tree() returns a boolean value which indicates whether a tree is the root tree in a Tree hierarchy, while root_tree() returns the root tree.

Instead of directly accessing the children field of a Tree widget, it's probably best to access them using the children() function, inherited from the Container Widget.

The tree_owner field is defined only in subtrees, where it points to the TreeItem widget which holds the tree in question. The level field indicates how deeply nested a particular tree is; root trees have level 0, and each successive level of subtrees has a level one greater than the parent level. This field is set only after a Tree widget is actually mapped (i.e. drawn on the screen).

The 'selection_changed' signal will be emitted whenever the selection field of a Tree has changed. This happens when a child of the Tree is selected or deselected.

The select_child signal is emitted when a child of the Tree is about to get selected. This happens on calls to select_item(), select_child(), on all button presses and calls to item_toggle() and toggle(). It may sometimes be indirectly triggered on other occasions where children get added to or removed from the Tree.

The 'unselect_child' signal is emitted when a child of the Tree is about to get deselected. As of GTK 1.0.4, this seems to only occur on calls to unselect_item() or unselect_child(), and perhaps on other occasions, but not when a button press deselects a child (the 'select_child' signal is emitted instead), nor on emission of the 'toggle' signal by toggle().

To insert a tree item into a Tree, use:

$tree->insert( $tree_item, $position );

where $position is the position in the tree.

To remove items from a tree, use:

$tree->remove_items( @items );

Note that removing an item from a tree dereferences and (usually) destroys it and its subtree, if it has one, and all subtrees in that subtree. If you want to remove only one item, you can use remove().

To remove a series of items, use:

$tree->clear_items( $start, $end );

which removes the items from position $start to position $end from a Tree. The same warning about dereferencing applies here, as clear_items() simply constructs a list and passes it to remove_items().

To select the child at a position in the tree, use:

$tree->select_item( $postition );

which will emit the 'select_item' signal for the child at position $position, thus selecting the child (unless you unselect it in a signal handler).

Conversely, to unselect the child at a position, use:

$tree->unselect_item( $position );

which emits the 'unselect_item' signal for the child at position $position, thus unselecting the child.

To select a tree item, use:

$tree->select_child( $tree_item );

Which emits the 'select_item' signal for the child $tree_item, thus selecting it.

And to select a tree item, use:

$tree->unselect_child( $tree_item );

Which emits the 'unselect_item' signal for the child $tree_item, thus unselecting it.

If you want to get the position in the tree of a child, then

$tree->child_position( $child );

is for you. It returns the position in the tree of $child, unless $child is not in the tree, in which case it returns -1.

The selection mode of a tree can be set using:

$tree->set_selection_mode( $mode );

where the selection mode can be one of 'single' (the default), 'browse', 'multiple', or 'extended'. This is only defined for root trees, which makes sense, since the root tree "owns" the selection. Setting it for subtrees has no effect at all; the value is simply ignored.

The "view mode" can be set using:

$tree->set_view_mode( $mode );

The "view mode" can be either 'line' (the default) or 'item'. The view mode propagates from a tree to its subtrees when they are mapped, and shouldn't be set exclusively to a subtree (setting it on a subtree after it has been mapped, would have a somewhat unpredictable effect).

The term "view mode" is rather ambiguous - basically, it controls the way the highlight is drawn when one of a tree's children is selected. If it's 'line', the entire TreeItem widget is highlighted, while for 'item', only the child widget (usually the label) is highlighted.

Drawing of connecting lines between tree items can be toggled using:

$tree->set_view_lines( $show_lines );

If $show_lines is a true value, the connecting lines are drawn, and if it is a false value, then they aren't.

The root ree of a Tree object can be retreived using:

$tree->root_tree();

The selection list of the root tree can be retreived using:

$tree->selection();

37.1. Tree Item Widget

The TreeItem widget, like CListItem, is derived from Item, which in turn is derived from Bin. Therefore, the item itself is a generic container holding exactly one child widget, which can be of any type. The TreeItem widget has a number of extra fields, but the only one we need be concerned with is the subtree field.

The fields available for the TreeItem struct looks like this:

item
subtree
pixmaps_box
plus_pix_widget
minus_pix_widget
pixmaps
expanded

The pixmaps_box field is an EventBox which catches clicks on the plus/minus symbol which controls expansion and collapsing. The pixmaps field points to an internal data structure. Since you can always obtain the subtree of a TreeItem with the subtree() function it's probably advisable never to touch the insides of a TreeItem unless you know what you're doing.

A TreeItem usually holds a label, so the convenience function new_with_label Gtk::TreeItem() is provided. The same effect can be achieved using code like the following:

$tree_item = new Gtk::TreeItem();
$label = new Gtk::Label( $text );
$label->set_alignment( 0.0, 0.5 );

$tree_item->add( $label );
$show( $label );

As one is not forced to add a Label to a TreeItem, you could also add an HBox or an Arrow, or even a Notebook (though your app will likely be quite unpopular in this case) to the TreeItem.

If you remove all the items from a subtree, it will be destroyed and unparented, unless you reference it beforehand, and the TreeItem which owns it will be collapsed. So, if you want it to stick around, do something like the following:

$tree->ref();
$owner = $tree->tree_owner;
$tree->remove( $item );
if ( $tree->parent )
  {
    $tree->unref();
  }
else
  {
    $owner->expand();
    $owner->set_subtree( $tree );
  }

Finally, drag-n-drop does work with TreeItems. You just have to make sure that the TreeItem you want to make into a drag item or a drop site has not only been added to a Tree, but that each successive parent widget has a parent itself, all the way back to a toplevel or dialog window, when you call dnd_drag_set() or dnd_drop_set(). Otherwise, strange things will happen.

TreeItem inherits the 'select', 'deselect', and 'toggle' signals from Item. In addition, it adds two signals of its own, 'expand' and 'collapse'.

The 'expand' signal is emitted when the tree item's subtree is about to be expanded, that is, when the user clicks on the plus sign next to the item, or when the program calls expand().

The 'collapse' signal is emitted when the tree item's subtree is about to be collapsed, that is, when the user clicks on the minus sign next to the item, or when the program calls collapse().

$tree_item->remove_subtree();

This removes all of tree_item's subtree's children (thus unreferencing and destroying it, any of its children's subtrees, and so on...), then removes the subtree itself, and hides the plus/minus sign.

37.2. Tree Example

This is a somewhat more complex example than I normally include, but I felt it was appropriate. As well as demonstrating the Tree widget, this example also uses quite a bit of the CList widget. From this, we could easily add a button bar or menu, and add functionality such as delete, copy, or move a file, which would make this a lightweight file manager.

Tree Example Source

      
#!/usr/bin/perl -w

use 
Gtk
;
use 
strict
;

set_locale Gtk;
init Gtk;


my
 $false = 0;

my
 $true = 1;


my
 $root_dir = "/";

my
 @titles;


my
 $window;

my
 $pane;

my
 $vbox;

my
 $tree_scrolled_win;

my
 $list_scrolled_win;

my
 $entry;

my
 $tree;

my
 $leaf;

my
 $subtree;

my
 $item;

my
 $list;


# Create a window
$window = new Gtk::Window( 'toplevel' );
$window->set_usize( 725, 500 );
$window->set_title( "File Viewer" );
$window->signal_connect( "delete_event", 
sub
 { Gtk->
exit
( 0 ); } );

# Create a horizontal pane
$pane = new Gtk::HPaned();
$window->add( $pane );
$pane->set_handle_size( 10 );
$pane->set_gutter_size( 8 );
$pane->show();

# Create a VBox for the Entry and Tree Scrolled Window
$vbox = new Gtk::VBox( $false, 0 );
$pane->add1( $vbox );
$vbox->show();

# Create the Text Entry
$entry = new Gtk::Entry();
$vbox->pack_start( $entry, $false, $false, 4 );
$entry->signal_connect( 'activate', \&entry_activate );
$entry->show();

# Create a ScrolledWindow for the tree
$tree_scrolled_win = new Gtk::ScrolledWindow( undef, undef );
$tree_scrolled_win->set_usize( 150, 400 );
$vbox->pack_start( $tree_scrolled_win, $true, $true, 0 );
$tree_scrolled_win->set_policy( 'automatic', 'automatic' );
$tree_scrolled_win->show();

# Create a ScrolledWindow for the list
$list_scrolled_win = new Gtk::ScrolledWindow( undef, undef );
$pane->add2( $list_scrolled_win );
$list_scrolled_win->set_policy( 'automatic', 'automatic' );
$list_scrolled_win->show();

# Create root tree
$tree = new Gtk::Tree();
$tree_scrolled_win->add_with_viewport( $tree );
$tree->set_selection_mode( 'single' );
$tree->set_view_mode( 'item' );
$tree->show();

# Create root tree item widget
$leaf = new_with_label Gtk::TreeItem( $root_dir );
$tree->append( $leaf );
$leaf->signal_connect( 'select', \&select_item, $root_dir );
$leaf->set_user_data( $root_dir );
$leaf->show();

# Create the subtree

if
 ( has_sub_trees( $root_dir ) )
{
   $subtree = new Gtk::Tree();
   $leaf->set_subtree( $subtree );
   $leaf->signal_connect( 'expand', \&expand_tree, $subtree );
   $leaf->signal_connect( 'collapse', \&collapse_tree );
   $leaf->expand();
}

# Create list box
@titles = qw( Filename Size Permissions Owner Group Time Date );
$list = new_with_titles Gtk::CList( @titles );
$list_scrolled_win->add( $list );
$list->set_column_width( 0, 100 );
$list->set_column_width( 1, 50 );
$list->set_column_width( 2, 80 );
$list->set_column_width( 3, 50 );
$list->set_column_width( 4, 50 );
$list->set_column_width( 5, 50 );
$list->set_column_width( 6, 100 );
$list->set_selection_mode( 'multiple' );
$list->set_shadow_type( 'none' );
$list->show();

$window->show();
main Gtk;

exit
( 0 );



### Subroutines


# Callback for expanding a tree - find subdirectories and add them to
# tree


sub
 
expand_tree

{
   
my
 ( $item, $subtree ) = @_;

   
my
 $dir;
   
my
 $dir_entry;
   
my
 $path;
   
my
 $item_new;
   
my
 $new_subtree;

   $dir = $item->get_user_data();

   
chdir
( $dir );

   
foreach
 $dir_entry ( <*> )
   {
      
if
 ( -d $dir_entry )
      {
	 $path = $dir . "/" . $dir_entry;
	 $path =~ s|//|/|g;
	 $item_new = new_with_label Gtk::TreeItem( $dir_entry );
	 $item_new->set_user_data( $path );
	 $item_new->signal_connect( 'select', \&select_item, $path );
	 $subtree->append( $item_new );
	 $item_new->show();

	 
if
 ( has_sub_trees( $path ) )
	 {
	    $new_subtree = new Gtk::Tree();
	    $item_new->set_subtree( $new_subtree );
	    $item_new->signal_connect( 'expand',
				       \&expand_tree,
				       $new_subtree );
	    $item_new->signal_connect( 'collapse', \&collapse_tree );
	 }
      }
   }

   
chdir
( ".." );
}


# Callback for collapsing a tree -- removes the subtree

sub
 
collapse_tree

{
   
my
 ( $item ) = @_;

   
my
 $subtree = new Gtk::Tree();

   $item->remove_subtree();
   $item->set_subtree( $subtree );
   $item->signal_connect( 'expand', \&expand_tree, $subtree );
}


# Test whether a directory has subdirectories

sub
 
has_sub_trees

{
   
my
 ( $dir ) = @_;
   
my
 $file;

   
foreach
 $file ( <$dir/*> )
   {
      
return
 $true 
if
 ( -d $file );
   }

   
return
 ( $false );
}


# Called whenever an item is clicked on the tree widget.

sub
 
select_item

{
   
my
 ( $widget, $path ) = @_;

   $entry->set_text( $path );

   show_files( $path );
}


# Called when enter is pressed in the Entry widget

sub
 
entry_activate

{
   
my
 ( $entry ) = @_;

   
my
 $path = $entry->get_text();

   
if
 ( -d $path )
   {
      show_files( $path );
   }
   
else

   {
      $entry->set_text( "/" );
   }
}


# Show all the files in a path.


sub
 
show_files

{
   
my
 ( $path ) = @_;

   
my
 $file;

   $list->clear();

   
for
 $file ( <$path/*> )
   {
      
unless
 ( -d $file )
      {
	 
my
 @lstat = lstat( $file );
	 
my
 ( $mode, $uid, $gid, $size, $mtime) = ( @lstat )[ 2, 4, 5, 7, 9 ];

	 
my
 @time = localtime( $mtime );
	 
my
 ( $mon, $day, $year, $hour, $min ) = ( @time )[ 4, 3, 5, 2, 1 ];

	 $min = "0" . $min 
if
 ( $min < 10 );

	 
my
 $time = $hour . ":" . $min;
	 
my
 $month = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun",
		       "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" )[$mon];
	 
my
 $date = $month . " " . $day . ", " . ( 1900 + $year );

	 
my
 $user = getpwuid( $uid );
	 
my
 $group = getgrgid( $gid );

	 $file =~ s|/.*/||g;
	 $list->append( $file, $size, $mode, $user, $group, $time, $date );
      }
   }
}


# END EXAMPLE PROGRAM
      
   

Tree Example Screenshot