/usr/local/CPAN/X11-Motif/Outline.pm


package Outline;

use strict;
use X11::Motif;

sub IS_SELECTED () {  1 }; # -flags -- item is selected
sub IS_OPENED ()   {  2 }; # -flags -- item is open (folder contents displayed)
sub IS_FILTERED () {  4 }; # -flags -- item is filtered out (not in the outline)
sub IS_CACHED ()   {  8 }; # -flags -- item is cached in memory (loaded from external source)
sub IS_FOLDER ()   { 16 }; # -flags -- item is a folder
sub IS_ANCHOR ()   { 32 }; # -flags -- item is an anchor point for determining relative paths
sub IS_KEPT ()     { 64 }; # -flags -- item is kept even when throwning out the cache

sub new {
    my $class = shift;
    my $parent = shift;

    my $outline = [ ];
    my $self = {
	-tree => { -label => 'TOP',
		   -flags => IS_FOLDER | IS_OPENED | IS_CACHED,
		   -children => [ ] },
	-currentitem => undef,
	-outline => $outline,
	-widget => undef,
	-menu => undef,
	-lastpick => undef,
	-selection => [ ]
    };

    bless $self, $class;

    if (defined $parent) {
	my $scrolled_window = $parent->give(-ScrolledWindow);
	my $columns = shift;
	my $lined_area = $scrolled_window->give(-XpLinedArea, @_);
	my $i = 0;

	foreach my $col (@{$columns}) {
	    $lined_area->XpLinedAreaInsertOutlineColumn($i, $col, $self, \&Outline::handle_event);
	    ++$i;
	}

	$self->{-widget} = $lined_area;
    }

    $self;
}

sub import {
    my $module = shift;

    foreach my $sym (@_) {
	if ($sym eq ':flags') {
	    X11::Lib::export_pattern(\%Outline::, '^IS_');
	}
	else {
	    X11::Lib::export_symbol(\%Outline::, $sym);
	}
    }
}

sub canvas () {
    my $self = shift;

    $self->{-widget};
}

sub window () {
    my $self = shift;

    $self->{-widget}->XtParent;
}

sub register_popup_menu {
    my($self, $menu) = @_;

    $self->{-menu} = $menu;
}

sub redraw {
    my($self) = @_;

    my $w = $self->{-widget};
    if (defined $w) {
	$w->XpLinedAreaRedraw;
    }
}

# --------------------------------------------------------------------------------

my $_traversal_flags;
my $_traversal_sub;
my $_traversal_not;
my $_traversal_continue;
my $_traversal_always_descend;
my $_traversal_level;

sub _traverse_tree {
    my($parent, $parent_return) = @_;

    my $sibling_return;
    my $child_id = 0;

    foreach my $child (@{$parent->{-children}}) {
	if (!defined($_traversal_flags) or ($_traversal_not xor ($child->{-flags} & $_traversal_flags))) {
	    $_traversal_continue = 1;

	    $sibling_return = &{$_traversal_sub}($parent, $child, $child_id,
						 $parent_return, $sibling_return);

	    if ($_traversal_continue && ($child->{-flags} & IS_FOLDER)) {
		++$_traversal_level;
		_traverse_tree($child, $sibling_return);
		--$_traversal_level;
	    }
	}
	elsif ($_traversal_always_descend) {
	    if ($child->{-flags} & IS_FOLDER) {
		++$_traversal_level;
		_traverse_tree($child, $parent_return);
		--$_traversal_level;
	    }
	}

	++$child_id;
    }
}

sub _fast_traverse_tree {
    my($parent) = @_;

    foreach my $child (@{$parent->{-children}}) {
	&{$_traversal_sub}($parent, $child);
	if ($child->{-flags} & IS_FOLDER) {
	    _fast_traverse_tree($child);
	}
    }
}

sub traverse {
    my($self, $sub, $flags, $parent_return) = @_;

    $_traversal_flags = $flags;
    $_traversal_sub = $sub;
    $_traversal_not = 0;
    $_traversal_always_descend = 1;
    $_traversal_level = 0;

    _traverse_tree($self->{-tree}, $parent_return);
}

sub fast_traverse {
    my($tree, $sub) = @_;

    $_traversal_sub = $sub;

    _fast_traverse_tree($tree);
}

sub traverse_not {
    my($self, $sub, $flags, $parent_return) = @_;

    $_traversal_flags = $flags;
    $_traversal_sub = $sub;
    $_traversal_not = 1;
    $_traversal_always_descend = 1;
    $_traversal_level = 0;

    _traverse_tree($self->{-tree}, $parent_return);
}

sub traverse_pruned {
    my($self, $sub, $flags, $parent_return) = @_;

    $_traversal_flags = $flags;
    $_traversal_sub = $sub;
    $_traversal_not = 0;
    $_traversal_always_descend = 0;
    $_traversal_level = 0;

    _traverse_tree($self->{-tree}, $parent_return);
}

sub traverse_pruned_not {
    my($self, $sub, $flags, $parent_return) = @_;

    $_traversal_flags = $flags;
    $_traversal_sub = $sub;
    $_traversal_not = 1;
    $_traversal_always_descend = 0;
    $_traversal_level = 0;

    _traverse_tree($self->{-tree}, $parent_return);
}

# --------------------------------------------------------------------------------

sub add_toplevel {
    my $self = shift;

    foreach my $child (@_) {
	$self->add_child($self->{-tree}, $child);
    }
}

my $_reformat_outline;

sub _reformat {
    my($parent, $child) = @_;

    $child->{-indent} = $_traversal_level;
    $child->{-parent} = $parent;
    $child->{-row} = @{$_reformat_outline};

    push @{$_reformat_outline}, $child;

    if (!($child->{-flags} & IS_OPENED)) {
	$_traversal_continue = 0;
    }
}

sub reformat {
    my($self, $child) = @_;
    my $outline = $self->{-outline};
    my $widget = $self->{-widget};

    @{$outline} = ();
    $_reformat_outline = $outline;

    $self->traverse_pruned_not(\&_reformat, IS_FILTERED);

    if (defined $widget) {
	$widget->XpLinedAreaSetRows(0, scalar @{$outline});
	if ($child) {
	    $widget->XpLinedAreaScrollToRow($child->{-row} - 1);
	}
    }
}

sub _reparent {
    my($parent, $child) = @_;

    $child->{-parent} = $parent;
}

sub reparent {
    my($self, $tree) = @_;

    fast_traverse($tree, \&_reparent);
}

# --------------------------------------------------------------------------------

sub get_hooks {
    my($item) = @_;

    my $hook_load;
    my $hook_autosel;
    my $found = 0;

    while (defined $item) {
	if (!defined($hook_load) && defined($item->{-load})) {
	    $hook_load = $item->{-load};
	    ++$found;
	}
	if (!defined($hook_autosel) && defined($item->{-autosel})) {
	    $hook_autosel = $item->{-autosel};
	    ++$found;
	}
	last if ($found == 2);
	$item = $item->{-parent};
    }

    return ($hook_load, $hook_autosel);
}

sub get_row {
    my($self, $row) = @_;
    return $self->{-outline}[$row];
}

sub add_child {
    my($self, $tree, $child) = @_;

    $child->{-parent} = $tree;
    push @{$tree->{-children}}, $child;

    if (exists $child->{-children}) {
	$self->reparent($child);
    }
}

sub _forget_cache {
    my($parent, $child) = @_;

    my @new_grandchildren = ();
    if ($child->{-flags} & IS_CACHED) {
	$child->{-flags} &= ~IS_CACHED;
	if (exists $child->{-children}) {
	    foreach my $grandchild (@{$child->{-children}}) {
		if ($grandchild->{-flags} & IS_KEPT) {
		    push @new_grandchildren, $grandchild;
		}
	    }

	    # This could (will?) cause a memory leak because
	    # children have references to their parent, i.e. this
	    # is a cyclic structure.  Perl won't garbage collect
	    # the children even though they've been taken out of
	    # the tree.

	    @{$child->{-children}} = @new_grandchildren;
	}
    }
}

sub forget_cache {
    my($self, $child) = @_;

    if (!defined $child) {
	_forget_cache($child->{-parent}, $self->{-tree});
	fast_traverse($self->{-tree}, \&_forget_cache);
	$self->{-tree}{-flags} |= IS_CACHED;
    }
    else {
	_forget_cache($child->{-parent}, $child);
	fast_traverse($child, \&_forget_cache);
    }
}

sub open_child {
    my($self, $child, $keep_open) = @_;

    $self->{-currentitem} = $child;

    my $flags = $child->{-flags};
    my($hook_load, $hook_autosel) = get_hooks($child);

    $flags &= ~IS_FILTERED;

    if ($flags & IS_FOLDER) {
	if (($flags & IS_OPENED) && !$keep_open) {
	    $child->{-flags} &= ~IS_OPENED;
	}
	else {
	    $flags |= IS_OPENED;

	    if (!($flags & IS_CACHED)) {
		&{$hook_load}($self, $child);
		$flags |= IS_CACHED;
	    }

	    $child->{-flags} = $flags;

	    if ($hook_autosel) {
		&{$hook_autosel}($self, $child);
	    }
	}
    }
}

sub do_by_name {
    my($self, $name, $sub) = @_;

    my $current_item = $self->{-currentitem};

    if (defined $current_item) {
	foreach my $child (@{$current_item->{-children}}) {
	    if ($child->{-label} =~ /^$name/) {
		&{$sub}($self, $child);
		return $child;
	    }
	}
    }

    0;
}

sub _open_child_by_name {
    my($self, $child) = @_;

    $self->open_child($child, 1);
}

sub open_child_by_name {
    my($self, $name) = @_;

    return $self->do_by_name($name, \&_open_child_by_name);
}

sub open_path_from_root {
    my $self = shift;

    $self->{-currentitem} = $self->{-tree};

    foreach my $name (@_) {
	return if (!$self->open_child_by_name($name));
    }

    $self->{-currentitem};
}

sub select_child {
    my($self, $child, $bit) = @_;

    $bit ||= IS_SELECTED;
    $child->{-flags} |= $bit;
}

sub _select_child_by_name {
    my($self, $child) = @_;

    $self->select_child($child);
}

sub select_child_by_name {
    my($self, $name) = @_;

    return $self->do_by_name($name, \&_select_child_by_name);
}

# --------------------------------------------------------------------------------

sub activate_row {
    my($self, $row) = @_;
    my $child = $self->get_row($row);

    if (defined $child) {
	$self->open_child($child);
	$self->reformat();
    }
}

# --------------------------------------------------------------------------------

my $_selected_bit;

sub _clear_bit {
    my($parent, $child) = @_;
    $child->{-flags} &= ~$_selected_bit;
}

sub clear_deep_selection {
    my($self, $bit) = @_;
    $_selected_bit = $bit || IS_SELECTED;
    $self->traverse(\&_clear_bit);
}

sub clear_selection {
    my($self, $bit) = @_;

    $bit ||= IS_SELECTED;
    foreach my $element (@{$self->{-outline}}) {
	$element->{-flags} &= ~$bit;
    }
}

sub row_is_selected {
    my($self, $row, $bit) = @_;
    my $element = $self->{-outline}[$row];

    if (defined $element) {
	$bit ||= IS_SELECTED;
	$element->{-flags} & $bit;
    }
}

sub select_row {
    my($self, $row, $bit) = @_;
    my $element = $self->{-outline}[$row];

    if (defined $element) {
	$bit ||= IS_SELECTED;
	$element->{-flags} |= $bit;
    }
}

sub clear_row {
    my($self, $row, $bit) = @_;
    my $element = $self->{-outline}[$row];

    if (defined $element) {
	$bit ||= IS_SELECTED;
	$element->{-flags} &= ~$bit;
    }
}

sub toggle_row {
    my($self, $row, $bit) = @_;
    my $element = $self->{-outline}[$row];

    if (defined $element) {
	my $flags = $element->{-flags};
	$bit ||= IS_SELECTED;

	if ($flags & $bit) {
	    $flags &= ~$bit;
	}
	else {
	    $flags |= $bit;
	}

	$element->{-flags} = $flags;
    }
}

sub selection {
    my($self, $bit) = @_;
    my @selected_items = ();

    $bit ||= IS_SELECTED;
    foreach my $element (@{$self->{-outline}}) {
	if ($element->{-flags} & $bit) {
	    push @selected_items, $element;
	}
    }
    @selected_items;
}

# --------------------------------------------------------------------------------

sub handle_event {
    my($w, $self, $event, $click, $row, $col) = @_;
    my $type = $event->type;
    my $redraw = 4;

    if ($type == X::ButtonRelease) {
	my $button = $event->button;
	my $state = $event->state;
	my $lastpick = $self->{-lastpick};

	if ($button == 1) {
	    if ($state & X::ShiftMask) {
		if (defined $lastpick) {
		    if (!($state & X::ControlMask)) {
			$self->clear_selection();
		    }
		    if ($row < $lastpick) {
			while ($row <= $lastpick) {
			    $self->select_row($row);
			    ++$row;
			}
		    }
		    else {
			while ($row >= $lastpick) {
			    $self->select_row($row);
			    --$row;
			}
		    }
		    $redraw = 2;
		}
	    }
	    elsif ($state & X::ControlMask) {
		$self->toggle_row($row);
		$self->{-lastpick} = $row;
		$redraw = 1;
	    }
	    else {
		# optimize the selection redraw quite a bit -- have the
		# clear_selection routine return the rows cleared and then only
		# redraw those rows. -- FIXME

		if ($event->time->delta($self->{-lasttime}) < 400) {
		    $self->activate_row($row);
		}
		else {
		    $self->clear_selection();
		    $self->select_row($row);
		}

		$self->{-lastpick} = $row;
		$self->{-lasttime} = $event->time;
		$redraw = 2;
	    }
	}
    }
    elsif ($type == X::ButtonPress) {
	my $button = $event->button;

	if ($button == 3) {
	    my $menu = $self->{-menu};

	    if (defined $menu) {
		X::Motif::XmMenuPosition($menu, $event);
		$menu->Manage();
	    }
	}
    }

    $redraw;
}

1;