# Curses::Widgets::Menu.pm -- Menu Widgets # # (c) 2001, Arthur Corliss # # $Id: Menu.pm,v 1.103 2002/11/14 01:26:34 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::Menu - Menu Widgets =head1 MODULE VERSION $Id: Menu.pm,v 1.103 2002/11/14 01:26:34 corliss Exp corliss $ =head1 SYNOPSIS use Curses::Widgets::Menu; $menu = Curses::Widgets::Menu->new({ COLUMNS => 10, INPUTFUNC => \&scankey, FOREGROUND => undef, BACKGROUND => 'black', FOCUSSWITCH => "\t", X => 1, Y => 1, MENUS => { MENUORDER => [qw(File)], File => { ITEMORDER => [qw(Save Quit)], Save => \&Save, Quit => \&Quit, }, CURSORPOS => 'File', BORDER => 1, }); $menu->draw($mwh, 1); $menu->execute; See the Curses::Widgets pod for other methods. =head1 REQUIREMENTS =over =item Curses =item Curses::Widgets =item Curses::Widgets::ListBox =back =head1 DESCRIPTION Curses::Widgets::Menu provides simplified OO access to menus. Each item in a menu can be tied to a subroutine reference which is called when selected. =cut ##################################################################### # # Environment definitions # ##################################################################### package Curses::Widgets::Menu; use strict; use vars qw($VERSION @ISA); use Carp; use Curses; use Curses::Widgets; use Curses::Widgets::ListBox; ($VERSION) = (q$Revision: 1.103 $ =~ /(\d+(?:\.(\d+))+)/); @ISA = qw(Curses::Widgets); ##################################################################### # # Module code follows # ##################################################################### =head1 METHODS =head2 new (inherited from Curses::Widgets) $menu = Curses::Widgets::Menu->new({ INPUTFUNC => \&scankey, FOREGROUND => undef, BACKGROUND => 'black', FOCUSSWITCH => "\t", MENUS => { MENUORDER => [qw(File)], File => { ITEMORDER => [qw(Save Quit)], Save => \&Save, Quit => \&Quit, }, CURSORPOS => 'File', BORDER => 1, }); The new method instantiates a new Menu object. The only mandatory key/value pairs in the configuration hash are B and B. All others have the following defaults: Key Default Description ============================================================ INPUTFUNC \&scankey Function to use to scan for keystrokes FOREGROUND undef Default foreground colour BACKGROUND 'black' Default background colour FOCUSSWITCH "\t" Characters which signify end of input MENUS {} Menu structure CURSORPOS '' Current position of the cursor BORDER 0 Avoid window borders The B option is a hash of hashes, with each hash a separate menu, and the constituent hashes being a Entry/Function pairs. Each hash requires a special key/value pair that determines the order of the items when displayed. Each item is separated by two spaces. =cut sub _conf { # Validates and initialises the new Menu object. # # Internal use only. my $self = shift; my %conf = ( INPUTFUNC => \&scankey, FOREGROUND => undef, BACKGROUND => 'black', FOCUSSWITCH => "\t", MENUS => {MENUORDER => []}, BORDER => 0, EXIT => 0, CURSORPOS => '', @_ ); my $err = 0; # Set the default CURSORPOS if undefined $conf{CURSORPOS} = $conf{MENUS}{MENUORDER}[0] unless defined $conf{CURSORPOS}; # Make sure no errors are returned by the parent method $err = 1 unless $self->SUPER::_conf(%conf); # Get the updated conf hash %conf = (); %conf = %{$self->{CONF}}; # Create a listbox as our popup menu $self->{LISTBOX} = Curses::Widgets::ListBox->new({ X => 0, Y => 0, LISTITEMS => [], FOREGROUND => $conf{FOREGROUND}, BACKGROUND => $conf{BACKGROUND}, LINES => 3, COLUMNS => 10, FOCUSSWITCH => "\n\e", INPUTFUNC => $conf{INPUTFUNC}, }) unless $err; return $err == 0 ? 1 : 0; } =head2 draw $menu->draw($mwh, 1); The draw method renders the menu 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 selection cursor to be rendered as well. =cut sub draw { my $self = shift; my $mwh = shift; my $active = shift; my $conf = $self->{CONF}; my ($y, $x); # Get the parent window's (max|beg)yx and save the info $mwh->getmaxyx($y, $x); $$conf{COLUMNS} = $x; $mwh->getbegyx($y, $x); $self->{BEGYX} = [$y, $x]; # Call the parent's draw method return $self->SUPER::draw($mwh, $active); } sub _geometry { my $self = shift; my $conf = $self->{CONF}; my @rv = (1, $$conf{COLUMNS}, 0, 0); if ($$conf{BORDER}) { $rv[1] -= 2; @rv[2,3] = (1, 1); } return @rv; } sub _cgeometry { my $self = shift; my $conf = $self->{CONF}; my @rv = (1, $$conf{COLUMNS}, 0, 0); $rv[1] -= 2 if $$conf{BORDER}; return @rv; } sub _border { # Make sure no one tries to call this on a menu } sub _caption { # Make sure no one tries to call this on a menu } sub _content { my $self = shift; my $dwh = shift; my $conf = $self->{CONF}; my $menu = $$conf{MENUS}; my $label; # Print the labels $label = join(' ', @{$$menu{MENUORDER}}); carp ref($self), ": Window not wide enough to display all menus!" if length($label) > $$conf{COLUMNS} - 2 * $$conf{BORDER}; $dwh->addstr(0, 0, $label); } sub _cursor { my $self = shift; my $dwh = shift; my $conf = $self->{CONF}; my $menu = $$conf{MENUS}; my $pos = $$conf{CURSORPOS}; my ($x, $label); # Get the x coordinate of the cursor and display the cursor $label = join(' ', @{$$menu{MENUORDER}}); if ($label =~ /^(.*\b)\Q$pos\E\b/) { $x = length($1); $dwh->chgat(0, $x, length($pos), A_STANDOUT, select_colour( @$conf{qw(FOREGROUND BACKGROUND)}), 0); } $self->_restore($dwh); } =head2 popup $menu->popup; This method causes the menu to be displayed. Since, theoretically, the menu should never be seen unless it's being actively used, we will always assume that we need to draw a cursor on the list as well. =cut sub popup { my $self = shift; my $conf = $self->{CONF}; my ($x, $y, $border) = (@$conf{qw(X Y)}, 1); my $lb = $self->{LISTBOX}; my ($pwh, $items, $cp, $in, $rv, $l); # Calculate the border column/lines $border *= 2; # Create the popup window unless ($pwh = newwin($lb->getField('LINES') + $border, $lb->getField('COLUMNS') + $border, $y, $x)) { carp ref($self), ": Popup creation failed, possible geometry problems"; return; } $pwh->keypad(1); # Render the list box $rv = $lb->execute($pwh); # Release the window $pwh->delwin; # Exit now if $rv is an escape return undef if $rv =~ /\e/; # Return the menu selection ($cp, $items) = $lb->getField(qw(CURSORPOS LISTITEMS)); return $$items[$cp] if (defined $cp && scalar @$items); } sub input_key { # Process input a keystroke at a time. # # Internal use only. my $self = shift; my $in = shift; my $conf = $self->{CONF}; my $lb = $self->{LISTBOX}; my ($menus, $pos) = @$conf{qw(MENUS CURSORPOS)}; my ($width, $height, $x, $y, $i, $j, $item, $rv, $sub, $l); return unless @{$$menus{MENUORDER}}; # Get the current menu index $i = 0; while ($i < @{$$menus{MENUORDER}} && $$menus{MENUORDER}[$i] ne $pos) { $i++ }; $item = $$menus{MENUORDER}[$i]; # Process special keys if ($in eq KEY_LEFT) { --$i; $i = $#{$$menus{MENUORDER}} if $i < 0; } elsif ($in eq KEY_RIGHT) { ++$i; $i = 0 if $i > $#{$$menus{MENUORDER}}; # Display the Menu } elsif ($in eq KEY_DOWN || $in eq "\n") { # Calculate and set popup geometry $x = 0; for (0..$i) { $x += (length($$menus{MENUORDER}[$i]) + 2) if $_ != $i; } $x += 1 if $$conf{BORDER}; $x += $self->{BEGYX}->[1]; $y = $$conf{BORDER} ? 2 : 1; $y += $self->{BEGYX}->[0]; @$conf{qw(Y X)} = ($y, $x); $l = 0; foreach (@{$$menus{$item}{ITEMORDER}}) { $l = length($_) if $l < length($_) }; $lb->setField( LISTITEMS => [ @{$$menus{$item}{ITEMORDER}} ], LINES => scalar @{$$menus{$item}{ITEMORDER}}, COLUMNS => $l, CURSORPOS => 0, ); # Display the popup $rv = $self->popup; if (defined $rv) { $$conf{EXIT} = 1; # Execute the reference { no strict 'refs'; $sub = $$menus{$item}{$rv}; if (defined $sub) { &$sub(); } else { carp ref($self), ": undefined subroutine ($rv) call attempted"; } } } # Process normal key strokes } else { beep(); } # Save the changes $pos = $$menus{MENUORDER}[$i]; $$conf{CURSORPOS} = $pos; } =head2 execute $menu->execute; This method acts like the standard Curses::Widgets method of the same name, with the exception being that selection of any menu item will also cause it to exit (having already called the associated item subroutine). =cut sub execute { my $self = shift; my $mwh = shift; my $conf = $self->{CONF}; my $menus = $$conf{MENUS}; my $func = $$conf{'INPUTFUNC'} || \&scankey; my $regex = $$conf{'FOCUSSWITCH'}; my $key; # Don't execute unless we have menus to interact with return unless @{$$menus{MENUORDER}}; # Set the initial focused menu to the first in the list $$conf{CURSORPOS} = $$menus{MENUORDER}[0]; $$conf{EXIT} = 0; $self->draw($mwh, 1); # Enter the scan loop while (1) { $key = &$func($mwh); if (defined $key) { if (defined $regex) { return $key if ($key =~ /^[$regex]/ || ($regex =~ /\t/ && $key eq KEY_STAB)); } $self->input_key($key); } return $key if $$conf{EXIT}; $self->draw($mwh, 1); } } 1; =head1 HISTORY =over =item 2002/10/17 -- First implementation =back =head1 AUTHOR/COPYRIGHT (c) 2001 Arthur Corliss (corliss@digitalmages.com) =cut