# Curses::Widgets::TextMemo.pm -- Text Memo Widgets # # (c) 2001, Arthur Corliss # # $Id: TextMemo.pm,v 1.104 2002/11/14 01:27:31 corliss Exp corliss $ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # ##################################################################### =head1 NAME Curses::Widgets::TextMemo - Text Memo Widgets =head1 MODULE VERSION $Id: TextMemo.pm,v 1.104 2002/11/14 01:27:31 corliss Exp corliss $ =head1 SYNOPSIS use Curses::Widgets::TextMemo; $tm = Curses::Widgets::TextMemo->new({ CAPTION => 'Memo', CAPTIONCOL => 'blue', COLUMNS => 10, MAXLENGTH => undef, LINES => 3, MASK => undef, VALUE => '', INPUTFUNC => \&scankey, FOREGROUND => 'white', BACKGROUND => 'black', BORDER => 1, BORDERCOL => 'red', FOCUSSWITCH => "\t", CURSORPOS => 0, TEXTSTART => 0, PASSWORD => 0, X => 1, Y => 1, READONLY => 0, }); $tm->draw($mwh, 1); See the Curses::Widgets pod for other methods. =head1 REQUIREMENTS =over =item Curses =item Curses::Widgets =back =head1 DESCRIPTION Curses::Widgets::TextMemo provides simplified OO access to Curses-based single line text fields. Each object maintains its own state information. =cut ##################################################################### # # Environment definitions # ##################################################################### package Curses::Widgets::TextMemo; use strict; use vars qw($VERSION @ISA); use Carp; use Curses; use Curses::Widgets; ($VERSION) = (q$Revision: 1.104 $ =~ /(\d+(?:\.(\d+))+)/); @ISA = qw(Curses::Widgets); ##################################################################### # # Module code follows # ##################################################################### =head1 METHODS =head2 new (inherited from Curses::Widgets) $tm = Curses::Widgets::TextMemo->new({ CAPTION => 'Memo', CAPTIONCOL => 'blue', COLUMNS => 10, MAXLENGTH => undef, LINES => 3, MASK => undef, VALUE => '', INPUTFUNC => \&scankey, FOREGROUND => 'white', BACKGROUND => 'black', BORDER => 1, BORDERCOL => 'red', FOCUSSWITCH => "\t", CURSORPOS => 0, TEXTSTART => 0, PASSWORD => 0, X => 1, Y => 1, READONLY => 0, }); The new method instantiates a new TextMemo object. The only mandatory key/value pairs in the configuration hash are B and B. All others have the following defaults: Key Default Description ============================================================ CAPTION undef Caption superimposed on border CAPTIONCOL undef Foreground colour for caption text COLUMNS 10 Number of columns displayed MAXLENGTH undef Maximum string length allowed LINES 3 Number of lines in the window VALUE '' Current field text INPUTFUNC \&scankey Function to use to scan for keystrokes FOREGROUND undef Default foreground colour BACKGROUND undef Default background colour BORDER 1 Display a border around the field BORDERCOL undef Foreground colour for border FOCUSSWITCH "\t" Characters which signify end of input CURSORPOS 0 Starting position of the cursor TEXTSTART 0 Line number of string to start displaying PASSWORD 0 Subsitutes '*' instead of characters READONLY 0 Prevents alteration to content The B is only valid when the B is enabled. If the border is disabled, the field will be underlined, provided the terminal supports it. The B has no effect if left undefined. =cut sub _conf { # Validates and initialises the new TextMemo object. # # Usage: $self->_conf(%conf); my $self = shift; my %conf = ( COLUMNS => 10, MAXLENGTH => undef, LINES => 3, VALUE => '', INPUTFUNC => \&scankey, BORDER => 1, UNDERLINE => 1, FOCUSSWITCH => "\t", CURSORPOS => 0, TEXTSTART => 0, PASSWORD => 0, READONLY => 0, @_ ); my @required = qw(X Y); my $err = 0; # Check for required arguments foreach (@required) { $err = 1 unless exists $conf{$_} }; # Make sure no errors are returned by the parent method $err = 1 unless $self->SUPER::_conf(%conf); return $err == 0 ? 1 : 0; } =head2 draw $tm->draw($mwh, 1); The draw method renders the text memo in its current state. This requires a valid handle to a curses window in which it will render itself. The optional second argument, if true, will cause the field's text cursor to be rendered as well. =cut sub _border { my $self = shift; my $dwh = shift; my $conf = $self->{CONF}; my ($border, $ts, $pos, $value, $lines) = @$conf{qw(BORDER TEXTSTART CURSORPOS VALUE LINES)}; my (@lines, $v, $i, $y, $x); # Massage the value as needed, and split the result $value = '' unless defined $value; $value = substr($value, 0, $$conf{MAXLENGTH}) if defined $$conf{MAXLENGTH}; @lines = textwrap($value, $$conf{COLUMNS} - 1); # Adjust the cursor position and text start line if they're out of whack $pos = $pos < 0 ? 0 : ($pos > length($value) ? $pos = length($value) : $pos); $ts = $#lines if $ts > $#lines; $ts = 0 if $ts < 0; if ($ts > 0 && $pos < length(join('', @lines[0..($ts - 1)]))) { $v = length(join('', @lines[0..($ts - 1)])); $i = $ts - 1; until ($v <= $pos) { $v -= length($lines[$i]); --$i; } $ts = $i > 0 ? $i : 0; ++$ts unless $pos < length($lines[0]); } elsif ($ts + $lines - 1 < $#lines && $pos >= length(join('', @lines[0..($ts + $lines - 1)]))) { $v = length(join('', @lines[0..($ts + $lines - 1)])); $i = $ts + $lines; until ($v >= $pos) { $v += length($lines[$i]); ++$i; } $ts = $i - $lines; ++$ts if $pos == $v; } ++$ts if $pos == length($value) and $ts + $lines == @lines; # Save the adjust values @$conf{qw(TEXTSTART CURSORPOS VALUE)} = ($ts, $pos, $value); $self->{SPLIT} = [@lines]; # Render the border if ($border) { # Call the parent method $self->SUPER::_border($dwh); # Place the arrows $dwh->getmaxyx($y, $x); $dwh->addch(0, $x - 2, ACS_UARROW) if $ts > 0; $dwh->addch($y - 1, $x - 2, ACS_DARROW) if $#lines - $ts > $lines; } } sub _content { my $self = shift; my $dwh = shift; my $conf = $self->{CONF}; my ($border, $ts, $pos, $lines, $cols) = @$conf{qw(BORDER TEXTSTART CURSORPOS LINES COLUMNS)}; my @lines = @{$self->{SPLIT}}; my ($i, $j); # Print the lines $j = 0; for ($i = $ts; $i < $ts + $lines; $i++) { unless ($i > $#lines) { $$conf{PASSWORD} ? $dwh->addstr($j, 0, '*' x length($lines[$i])) : $dwh->addstr($j, 0, $lines[$i]) ; } # Underline each line if there's no border $dwh->chgat($j, 0, $cols, A_UNDERLINE, select_colour(@$conf{qw(FOREGROUND BACKGROUND)}), 0) unless $border; $j++; } } sub _cursor { my $self = shift; my $dwh = shift; my $conf = $self->{CONF}; my ($pos, $ts) = @$conf{qw(CURSORPOS TEXTSTART)}; my @lines = @{$self->{SPLIT}}; my $i = 0; my $v = 0; my $seg; $v = length(join('', @lines[0..($ts - 1)])) if $ts > 0; while ($ts + $i < $#lines && $v + length($lines[$ts + $i]) <= $pos) { $v += length($lines[$ts + $i]); ++$i; } $v = $pos - $v; #$i-- if $i > 0 and substr($$conf{VALUE}, $pos - 1, 1) eq "\n"; if ($pos == length($$conf{VALUE}) && substr($$conf{VALUE}, $pos - 1, 1) eq "\n") { ++$i; $v = 0; } $dwh->chgat($i, $v, 1, A_STANDOUT, select_colour(@$conf{qw(FOREGROUND BACKGROUND)}), 0); $self->_restore($dwh); } sub input_key { # Process input a keystroke at a time. # # Usage: $self->input_key($key); my $self = shift; my $in = shift; my $conf = $self->{CONF}; my ($value, $pos, $max, $ro, $ts) = @$conf{qw(VALUE CURSORPOS MAXLENGTH READONLY TEXTSTART)}; my @string = split(//, $value); my @lines = @{$self->{SPLIT}}; my ($snippet, $i, $lpos, $l); # Process special keys if ($in eq KEY_BACKSPACE) { return if $ro; if ($pos > 0) { splice(@string, $pos - 1, 1); $value = join('', @string); --$pos; } else { beep; } } elsif ($in eq KEY_RIGHT) { $pos < length($value) ? ++$pos : beep; } elsif ($in eq KEY_LEFT) { $pos > 0 ? --$pos : beep; } elsif ($in eq KEY_UP || $in eq KEY_DOWN || $in eq KEY_NPAGE || $in eq KEY_PPAGE) { # Exit early if there's no text unless (length($value) > 0) { beep; return; } # Get the text length up to the displayed window $snippet = $ts == 0 ? 0 : length(join('', @lines[0..($ts - 1)])); # Get the position of the cursor relative to the line it's on, # as well as the line index if ($pos == length($value)) { $l = $#lines; $lpos = length($lines[$#lines]); } else { $i = 0; while ($snippet + length($lines[$ts + $i]) <= $pos) { $snippet += length($lines[$ts + $i]); ++$i; } $l = $ts + $i; $lpos = $pos - $snippet; } # Process according to the key if ($in eq KEY_UP) { if ($l > 0) { if (length($lines[$l - 1]) >= $lpos) { $pos -= length($lines[$l - 1]); } else { $pos -= ($lpos + 1); } } else { beep; } } elsif ($in eq KEY_DOWN) { if ($l < $#lines) { if (length($lines[$l + 1]) >= $lpos) { $pos += length($lines[$l]); } else { $pos += ((length($lines[$l]) - $lpos) + length($lines[$l + 1]) - 1); } } else { beep; } } elsif ($in eq KEY_PPAGE) { if ($l >= $$conf{LINES}) { $pos -= length(join('', @lines[(1 + $l - $$conf{LINES})..($l - 1)])); if (length($lines[$l - $$conf{LINES}]) > $lpos) { $pos -= length($lines[$l - $$conf{LINES}]); } else { $pos -= ($lpos + 1); } } elsif ($l > 0) { if ($lpos > length($lines[0])) { $pos = length($lines[0]) - 1; } else { $pos = $lpos; } } else { beep; } } elsif ($in eq KEY_NPAGE) { if ($l <= $#lines - $$conf{LINES}) { $pos += length(join('', @lines[($l + 1) ..($l + $$conf{LINES} - 1)])); if (length($lines[$l + $$conf{LINES}]) >= $lpos) { $pos += (length($lines[$l + $$conf{LINES}]) + 1); } else { $pos += ((length($lines[$l]) - $lpos) + length($lines[$l + $$conf{LINES}]) - 1); } } elsif ($l < $#lines) { if (length($lines[$#lines]) > $lpos) { $pos = length($value) - (length($lines[$#lines]) - $lpos); } else { $pos = length($value); } } else { beep; } } } elsif ($in eq KEY_HOME) { $pos = 0; } elsif ($in eq KEY_END) { $pos = length($value); # Process other keys } else { return if $ro || $in !~ /^[[:print:]]$/; # Exit if it's a non-printing character return unless $in =~ /^[\w\W]$/; # Reject if we're already at the max length if (defined $max && length($value) == $max) { beep; return; # Append to the end if the cursor's at the end } elsif ($pos == length($value)) { $value .= $in; # Insert the character at the cursor's position } elsif ($pos > 0) { @string = (@string[0..($pos - 1)], $in, @string[$pos..$#string]); $value = join('', @string); # Insert the character at the beginning of the string } else { $value = "$in$value"; } # Increment the cursor's position ++$pos; } # Save the changes @$conf{qw(VALUE CURSORPOS TEXTSTART)} = ($value, $pos, $ts); } 1; =head1 HISTORY =over =item 1999/12/29 -- Original text field widget in functional model =item 2001/07/05 -- First incarnation in OO architecture =back =head1 AUTHOR/COPYRIGHT (c) 2001 Arthur Corliss (corliss@digitalmages.com) =cut