# # This file is part of Curses-Toolkit # # This software is copyright (c) 2011 by Damien "dams" Krotkine. # # This is free software; you can redistribute it and/or modify it under # the same terms as the Perl 5 programming language system itself. # use warnings; use strict; package Curses::Toolkit::Widget::ScrollArea; { $Curses::Toolkit::Widget::ScrollArea::VERSION = '0.210'; } # ABSTRACT: a scrollable area use parent qw(Curses::Toolkit::Widget::Container); use Params::Validate qw(SCALAR ARRAYREF HASHREF CODEREF GLOB GLOBREF SCALARREF HANDLE BOOLEAN UNDEF validate validate_pos); use Curses::Toolkit::Widget::VScrollBar; use Curses::Toolkit::Widget::HScrollBar; our @EXPORT_OK = qw(ScrollArea); our %EXPORT_TAGS = (all => [qw(ScrollArea)]); sub ScrollArea { 'Curses::Toolkit::Widget::ScrollArea' } sub new { my $class = shift; my $self = $class->SUPER::new(); my %params = validate( @_, { scrollbars_mode => { type => SCALAR, default => 'always' }, } ); # $self->{visibility_mode} = 'auto'; $self->{scroll_x} = 0; $self->{scroll_y} = 0; $self->{_display_h_scrollbar} = 0; $self->{_display_v_scrollbar} = 0; $self->{_main_child} = undef; $self->set_scrollbars_mode($params{scrollbars_mode}); return $self; } sub set_scrollbars_mode { my ($self, $mode) = @_; if ($mode eq 'always') { $self->{scrollbars_mode} = $mode; $self->{v_scrollbar} = Curses::Toolkit::Widget::VScrollBar->new()->set_scroll_area($self) ->set_name($self . '_vscrollbar'); # A bit hackish (especially the setting of iterator to undef. I should # stop using iterators anyway $self->_add_child_at_beginning($self->{v_scrollbar}); $self->{v_scrollbar}->_set_parent($self); $self->{v_scrollbar}->_set_iterator(undef); $self->{h_scrollbar} = Curses::Toolkit::Widget::HScrollBar->new()->set_scroll_area($self) ->set_name($self . '_hscrollbar'); # A bit hackish (especially the setting of iterator to undef. I should # stop using iterators anyway $self->_add_child_at_beginning($self->{h_scrollbar}); $self->{h_scrollbar}->_set_parent($self); $self->{h_scrollbar}->_set_iterator(undef); # because it's a container, needs to take care of rebuilding coordinates # from top to bottom $self->rebuild_all_coordinates(); } else { die "scrollbar mode '" . $mode . "' is not supported"; } } # overload add_widget to add it always as first child sub add_widget { my $self = shift; my ($child_widget) = validate_pos( @_, { isa => 'Curses::Toolkit::Widget' } ); defined $self->{_main_child} and die 'there is already a child widget'; # scrollbars children are always the firsts, so that widget ordering (for # events for instance), find the scrollbar before the real child $self->_add_child_at_end($child_widget); $child_widget->_set_parent($self); $self->{_main_child} = $child_widget; # because it's a Container, needs to take care of rebuilding coordinates # from top to bottom $self->rebuild_all_coordinates(); return $self; } sub scroll { my ($self, %params) = @_; # $self->{scroll_y} += 5; defined $params{x} and $self->{scroll_x} += $params{x}; defined $params{y} and $self->{scroll_y} += $params{y}; my $count = 0; defined $self->{v_scrollbar} and $count++; defined $self->{h_scrollbar} and $count++; my @children = $self->get_children(); if (@children > $count) { # there is a main child my $shape = $self->get_visible_shape_for_children; # main child is always the last, so that widget ordering (for events for # instance), find the scrollbar before the real child my $child_widget = $children[-1]; if (defined $child_widget) { # How much does the child widget want ? We don't specify a given size my $child_space = $child_widget->get_desired_space(); $child_space->width + $self->{scroll_x} < $shape->width() and $self->{scroll_x} = $shape->width() - $child_space->width; $child_space->height + $self->{scroll_y} < $shape->height() and $self->{scroll_y} = $shape->height() - $child_space->height; } } $self->{scroll_x} >=0 and $self->{scroll_x} = 0; $self->{scroll_y} >=0 and $self->{scroll_y} = 0; $self->rebuild_all_coordinates(); $self->needs_redraw; return $self; } # Returns the relative rectangle that a child widget can occupy. Overloads the # method from Curses::Toolkit::Widget::Container : we return the widget space, # minus the potential scrollbars # input : none # output : a Curses::Toolkit::Object::Coordinates object sub _get_available_space { my ($self) = @_; my $rc = $self->get_relatives_coordinates(); if ($self->{scrollbars_mode} eq 'always') { $rc->get_x2() > $rc->get_x1() && $rc->get_y2() > $rc->get_y1() or return Curses::Toolkit::Object::Coordinates->new_zero(); return Curses::Toolkit::Object::Coordinates->new( x1 => $rc->get_x1, y1 => $rc->get_x1, x2 => $rc->get_x2() - 1, y2 => $rc->get_y2() - 1, ); } else { die "scrollbar mode '" . $self->{scrollbars_mode} . "' is not supported"; } } sub draw { my ($self) = @_; if ($self->{scrollbars_mode} eq 'always') { # if ( defined ($self->{h_scrollbar}) ) { # my $c = $self->get_visible_shape; # # XXX FIXME This is a Hack # $self->{h_scrollbar}->{coordinates} = Curses::Toolkit::Object::Coordinates->new( # x1 => $c->get_x1(), y1 => $c->get_y2()-1, # x2 => $c->get_x2() - ( defined($self->{v_scrollbar}) ? 1 : 0), y2 => $c->get_y2(), # ); # # XXX FIXME This is a Hack # $self->{h_scrollbar}->{theme_name} = $self->get_theme_name; # $self->{h_scrollbar}->{theme} = $self->get_theme; # $self->{h_scrollbar}->draw(); # } } else { die "scrollbar mode '" . $self->{scrollbars_mode} . "' is not supported"; } return; } # rebuild the children coordinate, don't specify available space to children sub _rebuild_children_coordinates { my ($self) = @_; my $count = 0; defined $self->{v_scrollbar} and $count++; defined $self->{h_scrollbar} and $count++; my @children = $self->get_children(); @children > $count or return; # there were no main child # main child is always the last, so that widget ordering (for events for # instance), find the scrollbar before the real child my $child_widget = $children[-1]; defined $child_widget or return; my $shape = $self->get_visible_shape_for_children; # How much does the child widget want ? We don't specify a given size my $child_space = $child_widget->get_desired_space(); $self->{_display_h_scrollbar} = 1; $child_space->width <= $shape->width() and $self->{scroll_x} = 0, $self->{_display_h_scrollbar} = 0; $self->{_display_v_scrollbar} = 1; $child_space->height <= $shape->height() and $self->{scroll_y} = 0, $self->{_display_v_scrollbar} = 0; $self->{h_scrollbar} and $self->{h_scrollbar}->set_visible($self->{_display_h_scrollbar}); $self->{v_scrollbar} and $self->{v_scrollbar}->set_visible($self->{_display_v_scrollbar}); # scroll the space accordingly $child_space->set( x1 => $child_space->get_x1() + $self->{scroll_x}, y1 => $child_space->get_y1() + $self->{scroll_y}, x2 => $child_space->get_x2() + $self->{scroll_x}, y2 => $child_space->get_y2() + $self->{scroll_y}, ); # A Scroll Area always grants the desired space $child_widget->_set_relatives_coordinates($child_space); $child_widget->can('_rebuild_children_coordinates') and $child_widget->_rebuild_children_coordinates(); # take care of potential scrollbars if ( defined ($self->{v_scrollbar}) && $self->{_display_v_scrollbar} ) { my $c = $self->get_visible_shape; my ($child_widget) = $self->get_children(); my $child_rc = $child_widget->_get_relatives_coordinates(); if ($child_rc->height <= $c->height) { $self->{v_scrollbar}->set_fill(1); } else { $self->{v_scrollbar}->set_fill($c->height / $child_rc->height); } # XXX FIXME This is a Hack $self->{v_scrollbar}->{coordinates} = Curses::Toolkit::Object::Coordinates->new( x1 => $c->get_x2()-1, y1 => $c->get_y1(), x2 => $c->get_x2(), y2 => $c->get_y2() - ( defined($self->{h_scrollbar}) ? 1 : 0), ); # XXX FIXME This is a Hack $self->{v_scrollbar}->{theme_name} = $self->get_theme_name; $self->{v_scrollbar}->{theme} = $self->get_theme; # $self->{v_scrollbar}->draw(); } # take care of potential scrollbars if ( defined ($self->{h_scrollbar}) && $self->{_display_h_scrollbar}) { my $c = $self->get_visible_shape; # XXX FIXME This is a Hack $self->{h_scrollbar}->{coordinates} = Curses::Toolkit::Object::Coordinates->new( x1 => $c->get_x1, y1 => $c->get_y2()-1, x2 => $c->get_x2() - ( defined($self->{v_scrollbar}) ? 1 : 0), y2 => $c->get_y2(), ); # XXX FIXME This is a Hack $self->{h_scrollbar}->{theme_name} = $self->get_theme_name; $self->{h_scrollbar}->{theme} = $self->get_theme; # $self->{h_scrollbar}->draw(); } return $self; } # overload the visible shape to take scrollbars in account sub get_visible_shape_for_children { my ($self) = @_; my $shape = $self->get_visible_shape(); if ($shape->width > 0) { $shape->set( x2 => $shape->get_x2() - $self->{_display_v_scrollbar}); } if ($shape->height > 0) { $shape->set( y2 => $shape->get_y2() - $self->{_display_h_scrollbar}); } return $shape; } #sub get_visible_shape { # my ($self) = @_; # my $shape = $self->SUPER::get_visible_shape(); # $shape->set( x2 => $shape->get_x2()-1 ); # return $shape; #} sub get_desired_space { my ( $self, $available_space ) = @_; if (!defined $available_space) { my ($child_widget) = $self->get_children(); defined $child_widget or return Curses::Toolkit::Object::Coordinates->new_zero(); return $child_widget->get_desired_space(); } my $desired_space = $available_space->clone(); return $desired_space; } sub get_minimum_space { my ( $self, $available_space ) = @_; my ($child_widget) = $self->get_children(); defined $child_widget or return Curses::Toolkit::Object::Coordinates->new_zero(); return $child_widget->get_minimum_space(defined $available_space ? $available_space : ()); } sub get_child_occupied_space { my ($self) = @_; my ($child_widget) = $self->get_children(); defined $child_widget or return Curses::Toolkit::Object::Coordinates->new_zero(); return $child_widget->relatives_coordinates()->clone; } 1; __END__ =pod =head1 NAME Curses::Toolkit::Widget::ScrollArea - a scrollable area =head1 VERSION version 0.210 =head2 get_desired_space Given a coordinate representing the available space, returns the space desired The ScrollArea desires all the space available, so it returns the available space input : a Curses::Toolkit::Object::Coordinates object output : a Curses::Toolkit::Object::Coordinates object =head2 get_minimum_space Given a coordinate representing the available space, returns the minimum space needed to properly display itself input : a Curses::Toolkit::Object::Coordinates object output : a Curses::Toolkit::Object::Coordinates object =head1 AUTHOR Damien "dams" Krotkine =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2011 by Damien "dams" Krotkine. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut