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;