Troubleshooters.Com Presents


Linux Productivity Magazine

Volume 2 Issue 3, March 2003
Perl Tk, Part 2, The Picklist

Copyright (C) 2003 by Steve Litt. All rights reserved. Materials from guest authors copyrighted by them and licensed for perpetual use to Linux Productivity Magazine. All rights reserved to the copyright holder, except for items specifically marked otherwise (certain free software source code, GNU/GPL, etc.). All material herein provided "As-Is". User assumes all risk and responsibility for any outcome.


See also Troubleshooting Techniques of the Successful Technologist
and Rapid Learning: Secret Weapon of the Successful Technologist
by Steve Litt

[ Troubleshooters.Com | Back Issues |Troubleshooting Professional Magazine ]


 
Every CPU cycle you give me, I'll find a use for it. -- Jon "maddog" Hall

CONTENTS

Editor's Desk

By Steve Litt
Every programming environment claims to be rapid, but most fall woefully short of that goal. By far the quickest development environment I've used is Clarion 2.1 for DOS. From a menu entry you'd specify a picklist, and quickly insert the necessary fields. From the picklist, you'd specify a form, and quickly insert the necessary fields and prompts. The wizard, which was template driven, was data aware, so you just picked fields and tables. You could create a 5 table app in a day.

Prototyping environments are a dime a dozen, but Clarion went beyond prototyping in several ways. First, the finished product's ubiquitous menu->picklist->form->field->picklist... was quite useful in production.

Second, Clarion's template driven wizards enabled the programmer not only to create an app in Clarion, but to continually refine it in Clarion. Clarion had many points in which you could insert include files, to customize it beyond what the wizards could give you.

Since the demise of DOS, I've sorely missed Clarion 2.1. I used Clarion 4, and it was much better than other development environments, but it wasn't as quick and intuitive as 2.1.

Over the years I've pondered how to create a Clarion 2.1 substitute. I knew I'd need a generic picklist object, and maybe a generic form object, and definitely a form-field object designed to spawn a picklist. Of all these objects, the picklist seemed most challenging. I coded a little baby picklist in C++ once, and it was a mess.

Then one day LEAPster Brian Ashe gave a PHP presentation, in which he showed off a complete and generic PHP picklist, including field headers that sorted on their fields when clicked. A perusal of his code showed it to be simple and clean. My long-lost desire to recreate Clarion was reignited.

After initial attempts at making a webapp type clarion using Brian's picklist, it was clear that HTTP's statelessness was so problematic that serious development wouldn't be fast enough. I looked at Perl/Tk, and basically knocked off all the features of Brian's picklist.

That's not to say I knocked off Brian's code. His code was clean and crisp. He had the browser to do rendering, and PHP is a very high level language. My Perl/Tk picklist includes more features, but it is almost a thousand lines, much of which isn't as readable. But the complex code is encapsulated in Picklist.pm, so it's not the application programmer's problem. The application programmer instantiates the Picklist object, defines its fields, provides a routine to fill its rows, defines various picklist properties, and then calls it. Easy!

It will be easier still once somebody develops a picklist wizard for the application programmer. Much of the wizard can actually be written with the picklist itself. This is especially true if the Picklist object is modified so its buttons aren't hard coded, but simply an array of buttons each of which can take a callback routine and hotkeys.

The bottom line is that this Picklist object, when combined with an object supplying database awareness, a database aware form, a form field capable of calling a picklist, and a wizard to customize these various components, can create a programming environment almost as fast as Clarion.

I'd like to thank not only Brian Ashe, but also John Simpson and many other LEAPsters for helping me figure out how to create this picklist object.

This month's Linux Productivity Magazine covers the Perl/Tk picklist. The schedule for completing other components of my Rapid Application Development environment is unknown, but will probably be included in the "Steve Litt's Perls of Wisdom" subsite of Troubleshooters.Com. Meanwhile, enjoy Linux Productivity Magazine issue, and take pleasure at the cross platform wonders provided by the combination of Perl and Tk.
Steve Litt is the author of Samba Unleashed.   Steve can be reached at Steve Litt's email address.

Which Perl?

By Steve Litt
The purpose of the code in this month's LPM is as a programming tool for the programming contractor. The idea is an incredibly quick development environment enabling the developer to market his/her skills to small business. The software that developer uses must align with that goal.

If your target machine is Linux, obviously you'll use the Perl and Perl::Tk that came with the Linux distribution. If your target machine is Windows, you'll probably need to install Perl on that machine. Such installation must be quick and it must be legal.

When it comes to Perl for Windows, Activestate Perl is the Cadillac of the industry. All other things being equal, you'll want to install Activestate. The one possible glitch is this paragraph from the ActivePerl license agreement:

2. You may make and give away verbatim copies of this Package for personal use, or for use within your organization, provided that you duplicate all of the original copyright notices and associated disclaimers. You may not distribute copies of this Package, or copies of packages derived from this Package, to others outside your organization without specific prior written permission from ActiveState (although you are encouraged to direct them to sources from which they may obtain it for themselves).

I wrote ActiveState founder and CTO Dick Hardt about the effect of this paragraph upon a contract programmer, explaining how downloading ActivePerl at the customer site would be too time consuming. Dick wrote me back saying he felt that it would be fine if the contract programmer bought a single copy of Activestate ActiveCD, and from that single CD installed ActivePerl on all his customers' computers, with the following caveats:
  1. The contract programmer must be onsite. Otherwise ActivePerl should be downloaded.
  2. The contract programmer must not bundle his program with ActivePerl. If he wants to do that, he needs to cut a redistribution deal with Activestate.
I believe Dick's solution is perfect for the contract programmer. For the price of the ActiveCD ($39.95 + S & H) you can do business with many customers. I'd also like to point out that depending on your customer relationship, you might want to purchase an ActiveCD for each customer. This gives your customer the warm and fuzzy feeling that he has all necessary software.

If, for some reason, Dick's solution doesn't work for your situation, you can  download SiePerl (URL in URL's section). Its README file states it can be distributed either under GPL or Artistic license, so as I see it you can conduct your business with it.
Steve Litt is the author of " Troubleshooting Techniques of the Successful Technologist".  Steve can be reached at Steve Litt's email address .

Using the Picklist Object

By Steve Litt
As you read this article, keep in mind that it would be trivial to create a wizard to create a picklist within your app.

Here's what the picklist object looks like:
Screenshot of the Picklist

The preceding screenshot shows a list of records, each of which is comprised of columns. In this case the first column is the presidential sequence, the second is the first name, the third is the last name, and the fourth is the term in office. Above each column is a button identifying the column. Clicking on any one of these header buttons sorts the picklist by that column.

Below the list of records are prompts explaining hotkeys for the Add, Change, Delete, Pick and Escape actions. Below those prompts are the buttons for the Add, Change, Delete, Pick and Escape actions. The Escape action returns to the caller with a value of -999. The Pick action returns to the caller with the value of the key field of the chosen record. The Add, Change and Delete actions proceed forward to a callback to perform the work of that action, after which control is returned to the picklist.

The following code is the implementation of the list of presidents in the previous screenshot. The following code will be explained later in this article.

#!/usr/bin/perl -w
#
#######################################################################
# Copyright (C) 2003 by Steve Litt, all rights reserved.
# Licensed under version 2 of the GNU General Public License.
# See http://www.troubleshooters.com/license.txt
#   or
# http://www.gnu.org/licenses/gpl.txt
#
# ABSOLUTELY NO WARRANTY, USE AT YOUR OWN RISK!
#

require 5.003 ;
use strict ;

use Picklist ;


############################################################
# Construct a picklist using the Picklist Package
#
package Main ;
use Tk ;

########################################
# Subroutine to call when user clicks the delete button on the picklist
#
sub myDeleteButtonEvent ( $$ ) 
	{ 
	my $arg1 = shift ;
	my $rowNum = shift ;
	print "In myDeleteButtonEvent , arg1=$arg1 , rowNum=$rowNum\n" ;
	} 

########################################
# Subroutine to call when user clicks the change button on the picklist
#
sub myChangeButtonEvent ( $$ ) 
	{ 
	my $arg1 = shift ;
	my $rowNum = shift ;
	print "In myChangeButtonEvent , arg1=$arg1 , rowNum=$rowNum\n" ;
	} 

########################################
# Subroutine to call when user clicks the add button on the picklist
#
sub myAddButtonEvent ( $$ ) 
	{ 
	my $arg1 = shift ;
	my $rowNum = shift ;
	print "In myAddButtonEvent , arg1=$arg1 , rowNum=$rowNum\n" ;
	} 

########################################
# This subroutine defines all the picklist's fields. Each field has a
# name to go in the header, a length to be displayed, a justification
# (right or left), a flag saying whether visible or invisible (it
# might be included to return a lookup value, but not be visible). 
#
# Additionally, exactly one field must be declared the KEY field, with
# the $picklist->setKeyFieldNumber() method, and one field must be
# declared the SORT field via the $picklist->setKeyFieldNumber()
# method.
#
sub defineMyPicklistFields ( $ ) 
	{ 
	my $picklist		= shift ;

	my $field = 0 ;

	########################################	
	# First field is president number
	# This field is the key returned from the picklist
	#
	$picklist->setFieldPrompt ( $field , "pNum" ) ;
	$picklist->setFieldLength ( $field , 5 ) ;
	$picklist->setFieldRightJustified ( $field ) ;
	$picklist->setFieldVisible ( $field ) ;
	$picklist->setKeyFieldNumber ( $field ) ;
	$field++ ;

	########################################	
	# Second field is president's first name
	#
	$picklist->setFieldPrompt ( $field , "Fname" ) ;
	$picklist->setFieldLength ( $field , 9 ) ;
	$picklist->setFieldLeftJustified ( $field ) ;
	$picklist->setFieldVisible ( $field ) ;
	$field++ ;

	########################################	
	# Third field is president's last name
	# This is the initial sort field
	#
	$picklist->setFieldPrompt ( $field , "Lname" ) ;
	$picklist->setFieldLength ( $field , 12 ) ;
	$picklist->setFieldLeftJustified ( $field ) ;
	$picklist->setFieldVisible ( $field ) ;
	$picklist->setSortFieldNumber ( $field ) ;
	$field++ ;

	########################################	
	# Forth field is president's term of office
	#
	$picklist->setFieldPrompt ( $field , "Term" ) ;
	$picklist->setFieldLength ( $field , 9 ) ;
	$picklist->setFieldLeftJustified ( $field ) ;
	$picklist->setFieldVisible ( $field ) ;
	$field++ ;
	} 

############################################################
# The loadMyPicklistWithRows() function does just what it says. It
# repeatedly calls $picklist->addRow(), each call with a reference to
# an array of all field values in the same order declared in the
# defineMyPicklistFields() routine.
#
sub loadMyPicklistWithRows ( $ ) 
	{ 
	my $picklist		= shift ;

	$picklist->addRow ( [ 43 , "George" , "Bush" , "2001-????" ] ) ;
	$picklist->addRow ( [ 42 , "Bill" , "Clinton" , "1993-2000" ] ) ;
	$picklist->addRow ( [ 41 , "George" , "Bush" , "1989-1992" ] ) ;
	$picklist->addRow ( [ 40 , "Ronald" , "Reagan" , "1981-1988" ] ) ;
	$picklist->addRow ( [ 39 , "Jimmy" , "Carter" , "1977-1980" ] ) ;
	$picklist->addRow ( [ 38 , "Gerald" , "Ford" , "1974-1976" ] ) ;
	$picklist->addRow ( [ 37 , "Richard" , "Nixon" , "1963-1968" ] ) ;
	$picklist->addRow ( [ 36 , "Lyndon" , "Johnson" , "1963-1968" ] ) ;
	$picklist->addRow ( [ 35 , "John" , "Kennedy" , "1961-1963" ] ) ;
	$picklist->addRow ( [ 34 , "Dwight" , "Eisenhower" , "1953-1960" ] ) ;
	$picklist->addRow ( [ 33 , "Harry" , "Truman" , "1945-1952" ] ) ;
	$picklist->addRow ( [ 32 , "Franklin" , "Roosevelt" , "1933-1945" ] ) ;
	} 

############################################################
# The main routine instantiates a picklist, calls
# defineMyPicklistFields() and loadMyPicklistWithRows(), sets the
# callbacks for the Add, Change and Delete buttons (none is needed for
# the Pick and Escape actions). It then builds a small window to call
# the picklist, and place the key value of the returned value in its
# label.
#
# By default the Add, Change, Delete, Pick and Escape actions are
# all enabled. If you want to disable some of those actions, use the
# disableAdd(), disableChange(), disableDelete(), disablePick(), or
# disableEscape() methods from the Picklist object.
#
# In practice, many picklists exist simply to pick a value, in which
# case you'd disable Add, Change and Delete. Others are not called
# from a field or form, but from the main program, to expose all
# records for add, change or delete. In that case you'd disable pick.
# And sometimes a picklist, called from a field to make a choice, also
# allows add, change and delete. In that case you'd disable nothing.
#
# Picklists objects have many more settings. See the Picklist.pm
# source code for details.
#
sub main() 
	{ 
	print "\n\n\n" ;

	########################################	
	# Build the Picklist object
	#
	my $picklist = Picklist->new() ;

	$picklist->setDefaultAction ( "PICK" ) ;

#	$picklist->disablePick() ;

	defineMyPicklistFields ( $picklist ) ;

	loadMyPicklistWithRows ( $picklist ) ;

	$picklist->setCallbackAdd ( [ \&Main::myAddButtonEvent , "dummy" ] ) ;
	$picklist->setCallbackChange ( [ \&Main::myChangeButtonEvent , "dummy" ] ) ;
	$picklist->setCallbackDelete ( [ \&Main::myDeleteButtonEvent , "dummy" ] ) ;

	########################################	
	# Build the main window, which calls the picklist
	#
	my $mw = MainWindow->new() ;

	my $label = $mw->Label ( -text => 'Init' )
		->pack ( -side => 'top' , -anchor => 'w' ) ;

	my $butOk= $mw->Button ( 
		-text => "Test" , 
		-command => sub { $label->configure ( -text =>
				$picklist->acquire ( $mw ) ) ; } 
		) ->pack() ->focus() ;

	my $butExit = $mw->Button ( 
		-text => "Quit" , 
		-command => sub { exit ; } 
		) ->pack() ;

	$mw->bind ( "" , sub { $butExit->invoke() } ) ;

	MainLoop() ;
	} 

main() ;


Look at the main routine of this program. You start by instantiating a Picklist object, and declare its default action to be the "Pick" action. What this means is that the Enter key and the doubleclick mouse action are assigned to the Pick action.

Next you call a subroutine you call defineMyPicklistFields(). This is a routine the Picklist user (i.e. the application programmer) creates to define each field shown in the Picklist object, and to assign the following properties to each field:
Additionally, the defineMyPicklistFields() routine sets two formwide variables that take field numbers -- the key field and the sort field. The key field is the field whose value in the selected row is returned to the caller on Pick action, or sent to the callback routine on Add, Change or Delete actions. The sort field is the field on which to sort the rows, and is obviously changed each time the user clicks one of the header buttons.

Once the defineMyPicklistFields()  routine sets all the field properties, you call the  loadMyPicklistWithRows()routine to load all the rows into the picklist. In this case this application programmer defined subroutine simply enumerates each row's field values. In a database app you'd probably write a routine to parse the table's field names, sequentially access some or all of its rows, and for each row sequence through the fields, placing them in an array reference and then using Picklist::addRow() on each array reference.

Now that the picklist's fields and data contents are completely defined, do the remaining administrative work. Use $picklist->setCallbackAdd(), $picklist->setCallbackChange(), $picklist->setCallbackDelete() to define functions implementing the Add, Change and Delete actions. In this particular case each of those functions is defined as a diagnostic stub, but typically the Add action would pull up a blank form suitable for completing and inserting, the Change action would pull up a form with existing data suitable for changing and rewriting, and the Delete action would pull up a read-only completed form serving as an "are you sure" dialog. I just realized as I'm writing this that I forgot provisions to read the new, changed or deleted record and update the picklist -- hopefully in a manner more efficient than rereading all records from the database. I leave that as a reader exercise.

That's it -- you're done. The rest of the main routine builts a simple window that calls the picklist (via $picklist->acquire()) and places the value of the returned key in a label on that window.

As I said earlier, this is more work than a VB, Powerbuilder or Delphi picklist. But it would be easy to write a wizard to create a picklist configurator, and once so configured, the picklist would be trivial to configure or copy and change using an editor.
Steve Litt is the author of Rapid Learning: Secret Weapon of the Successful Technologist. He can be reached atSteve Litt's email address.

Internal Data Format of the Picklist Object

By Steve Litt
To stand any chance of understanding the source code of the Picklist you need to understand its internal data format. And to understand that, you need to know the design criteria of the Picklist object.

The obvious intent of the picklist object is to list rows of data. Whether those rows come from a SQL statement, a flat file, or a programmer's hardcoded array is not important at this point.

Each listed row is comprised of fields, so the Picklist object is really a grid, except that you cannot access an individual field. Above the rows are headers for each field. Because a monospaced font is used, those headers are the same width as the fields' displayed data. The headers are actually buttons, and they're clickable. When you click on a header, the picklist is sorted by that field.

A row is chosen by single-clicking it, or by using the arrow keys to navigate to it. One of five actions can be taken on the chosen row:
  1. Add
  2. Change
  3. Delete
  4. Pick
  5. Escape
The programmer can disable any of these actions. Each action has a button to click, hotkeys to press, and help text showing those hotkeys. The priority here is quick input, accompanied by intuitiveness. This arrangement was inspired by the Clarion programming language.

The Add action calls a programmer specified callback routine which presumably queries the user for a record to add. The Change action calls a callback which probably will put the row in a form and give the user a chance to modify it. The Delete action typically displays the row's data in a read-only form, with an "Are you sure" prompt. Add, Change and Delete all return to the picklist when their callbacks terminate.

The Pick and Escape actions are different in that they terminate the Picklist and return a value to whatever called the picklist. In the case of the Escape action, a hard coded -999 is returned. In the case of the Pick action, the value of a certain field in the chosen row is returned. Which field is the "certain field"? That is set by the programmer, who declares one of the picklist's fields to be the "key field". It's the value of that field that's returned. Presumably that field is sufficient for the calling subroutine to look up the record.

Screen Widgets

Here's a screenshot of the picklist in this magazine's example:

Screenshot of the Picklist

As you can see, this screen is comprised of the following:
Any of the 5 actions can be disabled by the programmer. For instance, if the picklist is called to fill a field on a form, and the user is not allowed to add, change or delete choices, then the programmer would disable the Add, Change and Delete actions. If the picklist is called by the menu system rather than by a field on a form, the programmer would disable the Pick action. If an action is disabled, its button doesn't show up, nor does its help text.

Internal Representation of Field Properties

Notice I said field properties, not field data. The fields on the form are represented by a reference to an array. This array reference is internally represented as $self->{'fields'}. Each element of the referenced array is a hash, with the following elements:
$self->{'fields'}->[fieldnumber]->{'prompt'}
$self->{'fields'}->[fieldnumber]->{'visible'}
$self->{'fields'}->[fieldnumber]->{'justification'}
$self->{'fields'}->[fieldnumber]->{'length'}
Each field has a prompt (text displayed in its header button), a visibility (whether it appears on the Picklist), a left or right justification, and a display length. You might wonder why a field would be invisible. The answer is simple enough. The field might be included just because it's a unique key, capable of looking up the record, but the user has no reason to see this value. In this case you'd flag the field as invisible and as the key field, and then the field would be returned to the caller, even though the field is invisible.

There are numerous set and get routines to make adding fields and setting their properties easy. Most are shown in the showpicklist.pl program.

Internal Representation of Form-Wide Field Information

The form stores a single integer value representing the field number of the field to be used as the key field. The key field is set by the programmer at coding time, and never changes. In a database app it would typically be the primary key of a table. It must be unique for the application to work properly. Currently the only way to return a compound key is for the programmer to condense two fields into one. The key field is stored internally as $self->{'keyfield'}, and is outfitted with set and get routines.

Similarly, the Picklist object stores an integer field value representing the field by which to sort the rows. This value changes every time the user clicks a column header. The sort field is stored internally as $self->{'sortfield'}, and is outfitted with set and get routines.

Internal Representation of the Rows

Ughh! The rows are NOT stored as an array of strings. That's because information from various fields must be retrieved.

The rows are stored as a reference to an array of hashes, as follows:
$self->{'rows'};
Each element of the referenced array contains the following hash elements:
$self->{'rows'}->[$rowNum]->{'fields'}
$self->{'rows'}->[$rowNum]->{'sorttext'}
$self->{'rows'}->[$rowNum]->{'key'}
The {'fields'} part of the hash is, itself, a reference to an array comprised of the field values (not field properties -- field values).

You'll notice that the {'sorttext'} and {'key'} hash values are redundant, as they could be deduced by crossreferencing the {'fields'} part with the information in $self->{'fields'}, $self->{'keyfield'} and $self->{'sortfield'}. However, adding the two extra hash entries makes sorting and key reporting easier to code, and I think it might possibly make sorting faster too. It's possible that later versions of the Picklist object won't have these two extra hash entries.

Be sure you understand the data composition of the Picklist object, because without that understanding you don't stand a chance at understanding its source code.
Steve Litt is the author of the Universal Troubleshooting Process courseware.   Steve can be reached at Steve Litt's email address.

The Picklist Object's Source Code

By Steve Litt
As you read this code, keep in mind that the application programmer's needs are primarily restricted to instantiating the picklist, calling some of its set and get routines, and the following:

#######################################################################
# Copyright (C) 2003 by Steve Litt, all rights reserved.
# Licensed under version 1 of the 
#    Litt Perl Development Tools License
# See http://www.troubleshooters.com/licenses/LPDTL/COPYING.LPDTL.1.0
#
# ABSOLUTELY NO WARRANTY, USE AT YOUR OWN RISK!
#

require 5.003 ;
use strict;

######################################################################
# Package Picklist, implements a picklist.
#
# The picklist exposes the following actions:
#      Add
#      Change
#      Delete
#      Pick
#      Escape
#
# In all actions except the Escape action, the key field of the chosen
# record is either sent to the callback (Add, Change, Delete) or
# returned to the caller (Pick). The programmer configures the
# Picklist object, declaring one of the fields to be the key field. In
# database work, this would likely be the primary key of the table.
# Because you sometimes do not want to show the key field to the user,
# each field can be set invisible, in which case it won't show, nor
# can it be sorted on (except initially).
#
# The Pick action terminates the picklist and returns the chosen
# record to the caller. The Escape action terminates the picklist and
# returns the magic number -999. Obviously future versions should
# enable the user to specify the value returned on escape, but for the
# time being -999 is a pretty good value.
#
# The Add, Change and Delete actions all call a programmer defined
# callback function, and return to the picklist when that function
# terminates. Typically that function would be a form on which you
# could add or change a record, or a read-only form serving as a
# confirmation of delete.
#
# Each row of the picklist can contain many fields, and clicking the
# header button of that field will sort by that field.
#
package Picklist;
use Tk;

############################################################
# The Picklist object constructor. It primarily sets defaults.
#
sub new($)
	{
	my($type) = $_[0];
	my($self) = {};
	bless($self, $type);

	$self->{'fields'} = [];     # reference to an empty array

	$self->{'rows'} = [];       # reference to an empty array
	$self->{'nextrownum'} = 0;
	$self->{'keyfield'} = 0;
	$self->{'sortfield'} = 0;


	########################################
	# Screen vars
	#
	$self->{'plfont'} = ["Fixed", "14", "bold"];
	$self->{'helpfont'} = ["Fixed", "10", "bold"];
	$self->{'title'} = "Pick one...";
	$self->{'plheight'} = 0;
	$self->{'plwidth'} = 0;

	$self->{'addok'} = 1;
	$self->{'changeok'} = 1;
	$self->{'delok'} = 1;
	$self->{'pickok'} = 1;
	$self->{'escapeok'} = 1;

	$self->{'addkeys'} = ["","",""];
	$self->{'changekeys'} = [""];
	$self->{'delkeys'} = ["","","",""];
	$self->{'pickkeys'} = ["", "", ""];
	$self->{'escapekeys'} = ["", ""];

	$self->{'actionnames'} = ['ADD', 'CHANGE', 'DELETE', 'PICK', 'ESCAPE'];

	########################################
	# Callbacks
	#
	$self->{'callbackdel'} = [\&Picklist::nullCallback, $self];
	$self->{'callbackadd'} = [\&Picklist::nullCallback, $self];
	$self->{'callbackchange'} = [\&Picklist::nullCallback, $self];

	return($self);
	}

############################################################
# The runCallback() method is called from the Add, Change and
# Delete buttons. Because the button identified itself with
# either "ADD", "CHANGE", or "DELETE", the runCallback()
# method knows which programmer defined callback to call. 
#
sub runCallback($$)
	{
	my $self	= shift;
	my $rowNum	= shift;
	my $buttonText	= shift;
	my $cb;
	if($buttonText eq 'ADD')
		{
		$cb = $self->{'callbackadd'};
		}
	if($buttonText eq 'DELETE')
		{
		$cb = $self->{'callbackdel'};
		}
	if($buttonText eq 'CHANGE')
		{
		$cb = $self->{'callbackchange'};
		}
	$$cb[0]($$cb[1], $rowNum);
	}

############################################################
# The nullCallback() method is the default callback for the Add,
# Change and Delete buttons. It does nothing but print a diagnostic to
# stdout. It's up to the programmer to set these callbacks to
# something useful.
#
sub nullCallback($$$)
	{
	my $discard1 = shift;
	my $row = shift;
	print "Discard1 not defined\n" unless defined $discard1;
	print "Row not defined\n" unless defined $row;
	print "Information: Null Callback, button action not defined. \007row=$row\n" ;
	}

############################################################
# The makeArrayOfStrings() method uses $self->{'rows'},
# and $self->{'fields'} to construct an array of strings suitable for
# display in the ListBox widget of the Picklist.
#
sub makeArrayOfStrings($)
	{
	my $self		= shift;

	my @return = ();
	my $string = "";

	my @rows = @{$self->{'rows'}};
	foreach my $row (@rows)
		{
		my @fields = @{$row->{'fields'}};
		my $fieldNum = 0;
		my $string = "";
		foreach my $field (@fields)
			{
			if($self->isFieldVisible($fieldNum))
				{
				unless($string eq "")
					{
					$string .= " ";
					}
				$string .= $self->formatField($fieldNum, $field, ' ');
				}
			$fieldNum ++ ;
			}
		push(@return, ($string));
		}
	return(\@return);
	}

############################################################
# Given a Tk ListBox widget and a reference to an array of strings to
# display, the loadPickList() method loads the ListBox rows with
# elements of the array reference, after deleting all current rows of
# the ListBox widget.
#
sub loadPicklist($$$)
	{
	my $self		= shift;
	my $listbox		= shift;
	my $arrayref		= shift;

	my @array = @{$arrayref};

	$listbox->delete(0, 'end');
	$listbox->insert('end', @array);
	}

############################################################
# The onHeaderClick() method sorts the rows of the listbox by the
# field number passed it by the header button. This is how clicking on
# the header button sorts the picklist by its field.
#
sub onHeaderClick()
	{
	my $self		= shift;
	my $fieldNumber		= shift;
	my $listbox		= shift;

	$self->setSortFieldNumber ( $fieldNumber ) ;
	$self->sortRows();
	$self->loadPicklist ( $listbox , $self->makeArrayOfStrings() ) ;
	$listbox->activate ( 0 ) ;
	$listbox->selectionSet ( 0 ) ;
	}

######################################################################
######################################################################
# GRAPHICAL ROUTINES
#
########################################
# The following methods enable the programmer to set and query things
# like fonts, title, and picklist dimensions.
#
sub setPicklistFont($$){$_[0]->{'plfont'} = $_[1];}
sub getPicklistFont($) {return($_[0]->{'plfont'});}

sub setHelpFont($$){$_[0]->{'helpfont'} = $_[1];}
sub getHelpFont($) {return($_[0]->{'helpfont'});}

sub setTitle($$){$_[0]->{'title'} = $_[1];}
sub getTitle($){return($_[0]->{'title'});}

sub setPicklistHeight($$){$_[0]->{'plheight'} = $_[1];}
sub getPicklistHeight($) {return($_[0]->{'plheight'});}

sub setPlHeight($$){$_[0]->{'plheight'} = $_[1];}
sub getPlHeight($){return($_[0]->{'plheight'});}

sub setPlWidth($$){$_[0]->{'plwidth'} = $_[1];}
sub getPlWidth($){return($_[0]->{'plwidth'});}

########################################
# The enable, disable and can methods for the various actions enable
# the programmer to enable or disable individual actions, along with
# their buttons and help text, and also to find out whether a given
# action is enabled.
#
sub enableAdd($){$_[0]->{'addok'} = 1;}
sub enableChange($){$_[0]->{'changeok'} = 1;}
sub enableDel($){$_[0]->{'delok'} = 1;}
sub enablePick($){$_[0]->{'pickok'} = 1;}
sub enableEscape($){$_[0]->{'pickok'} = 1;}

sub disableAdd($){$_[0]->{'addok'} = 0;}
sub disableChange($){$_[0]->{'changeok'} = 0;}
sub disableDel($){$_[0]->{'delok'} = 0;}
sub disablePick($){$_[0]->{'pickok'} = 0;}
sub disableEscape($){$_[0]->{'pickok'} = 0;}

sub canAdd($){return($_[0]->{'addok'});}
sub canChange($){return($_[0]->{'changeok'});}
sub canDelete($){return($_[0]->{'delok'});}
sub canPick($){return($_[0]->{'pickok'});}
sub canEscape($){return($_[0]->{'escapeok'});}

########################################
# Each of the 5 actions (Add, Change, Delete, Pick and Escape) can
# have various hotkeys and mouse events. The following methods enable
# the programmer to set and query these hotkeys and mouse events. The
# get routine arguments and get routine returns are references to an
# array of strings, where each string is a Keysym such as ""
# or "" or "".
#
sub setAddKeys($$){$_[0]->{'addkeys'} = $_[1];}
sub getAddKeys($){return($_[0]->{'addkeys'});}

sub setChangeKeys($$){$_[0]->{'changekeys'} = $_[1];}
sub getChangeKeys($){return($_[0]->{'changekeys'});}

sub setDelKeys($$){$_[0]->{'delkeys'} = $_[1];}
sub getDelKeys($){return($_[0]->{'delkeys'});}

sub setPickKeys($$){$_[0]->{'pickkeys'} = $_[1];}
sub getPickKeys($){return($_[0]->{'pickkeys'});}

sub setEscapeKeys($$){$_[0]->{'escapekeys'} = $_[1];}
sub getEscapeKeys($){return($_[0]->{'escapekeys'});}

############################################################
# The deleteKeyString(), appendKeyString() and prependKeyString()
# methods are ways of deleting or adding a single Keysym string to an
# array reference of Keysym strings.
#
sub deleteKeyString($$$)
	{
	my $self		= shift;
	my $keyRef		= shift;
	my $stringToDelete	= shift;

	my @newArray = ();

	for my $string (@{$keyRef})
		{
		unless ( $string eq $stringToDelete )
			{
			push(@newArray, ($string));
			}
		}
	return(\@newArray);
	}

sub appendKeyString($$$)
	{
	my $self		= shift;
	my $keyRef		= shift;
	my $stringToAppend	= shift;
	push(@{$keyRef}, ($stringToAppend));
	return($keyRef);
	}

sub prependKeyString($$$)
	{
	my $self		= shift;
	my $keyRef		= shift;
	my $stringToAppend	= shift;
	unshift(@{$keyRef}, ($stringToAppend));
	return($keyRef);
	}

############################################################
# The setDefaultAction() method takes a string argument representing
# which action should be the default. It then goes through the Keysym
# array references, adding "", "", and
# "" to its list of hotkeys. This method also
# deletes those hotkeys from all the other actions.
#
sub setDefaultAction($$)
	{
	my $self		= shift;
	my $desiredActionString	= shift;

	my $originalKeyArray;
	foreach my $actionString (@{$self->{'actionnames'}})
		{
		$originalKeyArray = $self->getAddKeys() if $actionString eq 'ADD';
		$originalKeyArray = $self->getChangeKeys() if $actionString eq 'CHANGE';
		$originalKeyArray = $self->getDelKeys() if $actionString eq 'DELETE';
		$originalKeyArray = $self->getPickKeys() if $actionString eq 'PICK';
		$originalKeyArray = $self->getEscapeKeys() if $actionString eq 'ESCAPE';

		$originalKeyArray = $self->deleteKeyString($originalKeyArray, "");
		$originalKeyArray = $self->deleteKeyString($originalKeyArray, "");
		$originalKeyArray = $self->deleteKeyString($originalKeyArray, "");

		if ( $actionString eq $desiredActionString )
			{
			$originalKeyArray = $self->prependKeyString($originalKeyArray, "");
			$originalKeyArray = $self->prependKeyString($originalKeyArray, "");
			$originalKeyArray = $self->appendKeyString($originalKeyArray, "");
			}

		$self->setAddKeys($originalKeyArray) if $actionString eq 'ADD';
		$self->setChangeKeys($originalKeyArray) if $actionString eq 'CHANGE';
		$self->setDelKeys($originalKeyArray) if $actionString eq 'DELETE';
		$self->setPickKeys($originalKeyArray) if $actionString eq 'PICK';
		$self->setEscapeKeys($originalKeyArray) if $actionString eq 'ESCAPE';
		}
	}

######################################################################
# NonGraphical routines
#
############################################################
# The programmer is provided with set and get routines for the
# following field attributes:
#      Field prompt (the heading text)
#      Field visibility on the picklist
#      Field justification (left or right)
#      Field length (display length on the picklist)
#
sub setFieldPrompt($$$){$_[0]->{'fields'}->[$_[1]]->{'prompt'} = $_[2];}
sub getFieldPrompt($$){return($_[0]->{'fields'}->[$_[1]]->{'prompt'});}

sub setFieldVisible($$){$_[0]->{'fields'}->[$_[1]]->{'visible'} = 1;}
sub setFieldInVisible($$){$_[0]->{'fields'}->[$_[1]]->{'visible'} = 0;}
sub isFieldVisible($$){return($_[0]->{'fields'}->[$_[1]]->{'visible'});}

sub setFieldLeftJustified($$){$_[0]->{'fields'}->[$_[1]]->{'justification'} = "left";}
sub setFieldRightJustified($$){$_[0]->{'fields'}->[$_[1]]->{'justification'} = "right";}
sub getFieldJustification($$){return($_[0]->{'fields'}->[$_[1]]->{'justification'});}

sub isFieldLeftJustified($$) { return($_[0]->getFieldJustification($_[1]) eq 'left'); }
sub isFieldRightJustified($$) { return($_[0]->getFieldJustification($_[1]) eq 'right'); }

sub setFieldLength($$$){$_[0]->{'fields'}->[$_[1]]->{'length'} = $_[2];}
sub getFieldLength($$){return($_[0]->{'fields'}->[$_[1]]->{'length'});}

############################################################
# The programmer is provided with routines to set and get which field
# to sort by, and which field is considered the "Key field", in other
# words, which field is returned by the Pick action.
#
sub setKeyFieldNumber($$){$_[0]->{'keyfield'} = $_[1];}
sub getKeyFieldNumber($$){return($_[0]->{'keyfield'});}

sub setSortFieldNumber($$){$_[0]->{'sortfield'} = $_[1];}
sub getSortFieldNumber($$){return($_[0]->{'sortfield'});}

sub setCallbackDelete($$){$_[0]->{'callbackdel'} = $_[1];}
sub getCallbackDelete($$){return($_[0]->{'callbackdel'});}

sub setCallbackAdd($$){$_[0]->{'callbackadd'} = $_[1];}
sub getCallbackAdd($$){return($_[0]->{'callbackadd'});}

sub setCallbackChange($$){$_[0]->{'callbackchange'} = $_[1];}
sub getCallbackChange($$){return($_[0]->{'callbackchange'});}

############################################################
# The getKeyFromIndex() method translates the ListBox index number to
# the value of the Key field for the row represented by the ListBox
# widget index number.
#
sub getKeyFromIndex($$)
	{
	return $_[0]->{'rows'}->[$_[1]]->{'fields'}->[$_[0]->getKeyFieldNumber()];
	}

############################################################
# The char2string() routine makes a string $num long comprised
# entirely of character $char. There's probably a Perl function to do
# this, but I couldn't find it.
#
sub char2string($$$)
	{
	my $self		= shift;
	my $char		= shift;
	my $num			= shift;
	my $return = "";
	while($num > 0)
		{
		$return .= $char;
		$num -- ;
		}
	return($return);
	}

############################################################
# The formatField() method takes the field number, string to be
# formatted, and a fill character as arguments. Using the length
# associated with the field number, it justifies the string according
# to the justification associated with the field number, and right or
# left fills with the fill character. If the string is longer than the
# length associated with the field number, the field is truncated on
# the right.
#
sub formatField($$$$)
	{
	my $self		= shift;
	my $fieldNum		= shift;
	my $string		= shift;
	my $fillChar		= shift;

	my $length = $self->getFieldLength($fieldNum);
	if (length($string) > $length)
		{
		return(substr($string, 0, $length));
		}
	if($self->isFieldLeftJustified($fieldNum))
		{
		return (
			$string . 
			$self->char2string($fillChar, $length - length($string))
			);
		}
	else
		{
		return (
			$self->char2string($fillChar, $length - length($string))
			. $string  
			);
		}
	}

############################################################
# The addRow() method adds a row to the picklist.
#
sub addRow($$)
	{
	my $self		= shift;
	my $fieldArrayRef	= shift;

	my $sortFieldNum = $self->getSortFieldNumber();

	my @fields = @{$fieldArrayRef};
	
	my $rowNum = $self->{'nextrownum'};
	$self->{'nextrownum'} ++ ;
	
	my $row = $self->{'rows'}->[$rowNum];

	$row->{'sorttext'} =
		$self->formatField(
			$sortFieldNum,
			$fields[$sortFieldNum],
			' '
			);
	$row->{'key'} = $fields[$self->getKeyFieldNumber()];
	$row->{'fields'} = \@fields;
	$self->{'rows'}->[$rowNum] = $row ;
	}

############################################################
# The sortRows() method sorts the rows of the picklist, but does not
# redisplay them.
#
sub sortRows($)
	{
	my $self		= shift;

	my $sortFieldNum = $self->getSortFieldNumber();

	my @rows = @{$self->{'rows'}};
	foreach my $row (@rows)
		{
		$row->{'sorttext'} =
			$self->formatField(
				$sortFieldNum,
				$row->{'fields'}->[$sortFieldNum],
				' '
				);
		}
	@rows = sort
		{$a->{'sorttext'} cmp  $b->{'sorttext'}}
		@rows;
	$self->{'rows'} = \@rows;
	}


############################################################
# The showRows() method is a diagnostic to print the fields 
# of various rows to stdout.
#
sub showRows($)
	{
	my $self		= shift;

	print "Fields: ";

	print $self->char2string(
		" ",
		$self->getFieldLength($self->getSortFieldNumber()) + 7
		);
	print $self->char2string(" ", 2);
	my @fields = @{$self->{'fields'}};
	my $fieldNum = 0;
	foreach my $field (@fields)
		{
		if($self->isFieldVisible($fieldNum))
			{
			print ">" . 
			 $self->formatField($fieldNum, $self->getFieldPrompt($fieldNum), '-')
			 . "<";
			}
		else
			{
			# print $self->formatField($fieldNum, $fieldNum, ' ');
			}

		$fieldNum ++ ;
		}
	print "\n";

	my @rows = @{$self->{'rows'}};
	foreach my $row (@rows)
		{
		print "sort($row->{'sorttext'}), ";
		print "key($row->{'key'}), ";
		my @fields = @{$row->{'fields'}};
		my $fieldNum = 0;
		foreach my $field (@fields)
			{
			if($self->isFieldVisible($fieldNum))
				{
				print ">" .
					$self->formatField($fieldNum, $field, ' ') .
					"<";
				}
			else
				{
				# print ">" .
				#	$self->formatField($fieldNum, $fieldNum, ' ') .
				#	"<";
				}
			$fieldNum ++ ;
			}
		print "\n";
		}
	}


############################################################
# The acquire() method displays the picklist object and handles user
# interaction. This method is a 350 line monstrosity. In the time
# allotted, I couldn't think of a good way of carving it up into
# subroutines without getting so wound up in argument passing as to
# create more complexity. Perhaps you can figure a better design.
#
#
# typically called after the rows have been sorted.
#
sub acquire($$)
	{
	my $self		= shift;
	my $mw			= shift;

	my $return = -999;

	my $tlw;

	if ( defined ( $mw )  ) 
		{ 
		$tlw = $mw->Toplevel();
		} 
	else
		{ 
		$tlw = MainWindow->new();
		} 

	$tlw->title($self->getTitle());

	
	########################################
	# Carve window into top level frames
	#
	my $fTitle =   $tlw->Frame();
	my $fPick =    $tlw->Frame();
	my $fHelp =    $tlw->Frame();
	my $fButtons = $tlw->Frame();
	

	########################################
	# Prepare the form header
	#
	$fTitle->Label(-text=> $self->getTitle())
		->pack(-side=>'top', -anchor=>'center');
	$fTitle->pack(-side=>'top', -anchor=>'center');

	########################################
	# Prepare the Picklist
	#
	
	my $arrayref = $self->makeArrayOfStrings();


	####################
	# DEFAULT OPTIONAL ARGUMENTS
	#
	my @listContents = @{$arrayref};
	if ( $self->getPlHeight() == 0  ) 
		{ 
		$self->setPlHeight($#listContents);
		if ( $self->getPlHeight() > 30 ) { $self->setPlHeight(30); } 
		} 

	if ( $self->getPlWidth() == 0  ) 
		{ 
		my $width=1;	
		foreach my $line ( @listContents ) 
			{ 
			chomp ( $line ) ;
			my $temp = length ( $line ) ;
			if ( $temp > $width ) { $width = $temp; } 
			} 
		if ( $width > 90 ) { $width = 90; } 
		$self->setPlWidth($width);
		} 
	
	####################
	# Set up Picklist 
	#
	$fPick->pack(-side=>'top', -anchor=>'center');
	my $fphdr = $fPick->Frame()->pack(-side=>"top", -anchor=>"w");

	my $list = $fPick->Scrolled (
		'Listbox' ,
		-scrollbars => 'osoe',
		-font=>$self->getPicklistFont()
		);

	my $thisIsTheFirstField = 1;
	my $fieldNum = 0;
	foreach my $field (@{$self->{'fields'}})
		{
		if($self->isFieldVisible($fieldNum))
			{
			if ($thisIsTheFirstField == 1)
				{
				$thisIsTheFirstField = 1;
				}
			else
				{
				$fphdr->Label(
					-text=>" ",
					-borderwidth=>0,
					-highlightthickness=>0,
					-padx=>0,
					-font=>$self->getPicklistFont()
					)->pack(-side=>"left", -anchor=>'w');
				}

			my $temp = $self->formatField(
				$fieldNum,
				$self->getFieldPrompt($fieldNum),
				'-'
				);

			my $hdrbutton = $fphdr->Button(
				-text=>$temp,
				-borderwidth=>3,
				-highlightthickness=>1,
				-padx=>0,
				-relief=>'raised',
				-font=>$self->getPicklistFont(),
				-takefocus=>1,
				-command=>[\&onHeaderClick, $self, $fieldNum, $list]
#				-command=>sub{$self->onHeaderClick($fieldNum, $list);},
				)->pack(-side=>"left", -anchor=>'w');
			}
		$fieldNum ++ ;
		}

	$list->configure ( -height => $self->getPlHeight() , -width => $self->getPlWidth ) ;
	$list->configure ( -selectmode => "browse" ) ;
	$list->pack(-side=>"top", -anchor=>"w");

	####################
	# Populate Picklist 
	#
	$self->loadPicklist($list, $arrayref);
	$list->activate ( 0 ) ;
	$list->selectionSet ( 0 ) ;


	########################################
	# Prepare the help text
	#
	my $text = "";

	if($self->canAdd())
		{
		$text = "";
		foreach my $key (@{$self->{'addkeys'}})
			{
			if($text eq "")
				{
				$text = "To Add:     ";
				}
			else
				{
				$text .= ", ";
				}

			$text .= $key;
			}
		$fHelp->Label(-text=> $text, -font => $self->getHelpFont())
			->pack(-side=>'top', -anchor=>'w');
		}

	if($self->canChange())
		{
		$text = "";
		foreach my $key (@{$self->{'changekeys'}})
			{
			if($text eq "")
				{
				$text = "To Change:  ";
				}
			else
				{
				$text .= ", ";
				}

			$text .= $key;
			}
		$fHelp->Label(-text=> $text, -font => $self->getHelpFont())
			->pack(-side=>'top', -anchor=>'w', -pady => 0);
		}

	if($self->canDelete())
		{
		$text = "";
		foreach my $key (@{$self->{'delkeys'}})
			{
			if($text eq "")
				{
				$text = "To Delete:  ";
				}
			else
				{
				$text .= ", ";
				}

			$text .= $key;
			}
		$fHelp->Label(-text=> $text, -font => $self->getHelpFont())
			->pack(-side=>'top', -anchor=>'w');
		}


	if($self->canPick())
		{
		$text = "";
		foreach my $key (@{$self->{'pickkeys'}})
			{
			if($text eq "")
				{
				$text = "To Pick:    ";
				}
			else
				{
				$text .= ", ";
				}

			$text .= $key;
			}
		$fHelp->Label(-text=> $text, -font => $self->getHelpFont())
			->pack(-side=>'top', -anchor=>'w');
		}

	if($self->canEscape())
		{
		$text = "";
		foreach my $key (@{$self->{'escapekeys'}})
			{
			if($text eq "")
				{
				$text = "To Escape:  ";
				}
			else
				{
				$text .= ", ";
				}

			$text .= $key;
			}
		$fHelp->Label(-text=> $text, -font => $self->getHelpFont())
			->pack(-side=>'top', -anchor=>'w');
		}
	$fHelp->pack(-side=>'top', -anchor=>'center');

	########################################
	# Prepare the buttons
	#
	my $butAdd;
	my $butChange;
	my $butDelete;
	my $butPick;
	my $butEscape;

	if($self->canAdd())
		{
		$butAdd = $fButtons->Button(-text => 'Add')
			->pack(-side=>'left', -anchor=>'w', -padx=>20);
		$butAdd->configure(-command=>sub
			{
			my $fld = $self->getKeyFromIndex($list->curselection());
			$self->runCallback($fld, 'ADD')
			});
		$self->loadPicklist($list, $arrayref);
		$list->activate ( 0 ) ;
		$list->selectionSet ( 0 ) ;
		}
	if($self->canChange())
		{
		$butChange = $fButtons->Button(-text => 'Change')
			->pack(-side=>'left', -anchor=>'w', -padx=>20);
		$butChange->configure(-command=>sub
			{
			my $fld = $self->getKeyFromIndex($list->curselection());
			$self->runCallback($fld, 'CHANGE')
			});
		$self->loadPicklist($list, $arrayref);
		$list->activate ( 0 ) ;
		$list->selectionSet ( 0 ) ;
		}
	if($self->canDelete())
		{
		$butDelete = $fButtons->Button(-text => 'Delete')
			->pack(-side=>'left', -anchor=>'w', -padx=>20);
		$butDelete->configure(-command=>sub
			{
			my $fld = $self->getKeyFromIndex($list->curselection());
			$self->runCallback($fld, 'DELETE')
			});
		$self->loadPicklist($list, $arrayref);
		$list->activate ( 0 ) ;
		$list->selectionSet ( 0 ) ;
		}
	if($self->canPick())
		{
		$butPick = $fButtons->Button(-text => 'Pick')
			->pack(-side=>'left', -anchor=>'w', -padx=>20);
		$butPick->configure(-command=>sub
			{
			$return = $self->getKeyFromIndex($list->curselection());
			$tlw->destroy();
			});
		}
	if($self->canEscape())
		{
		$butEscape = $fButtons->Button(-text => 'Escape',
			-command=>sub{$tlw->destroy()})
			->pack(-side=>'left', -anchor=>'w', -padx=>20);
		}
	
	$fButtons->pack(-side=>'top', -anchor=>'center');

	########################################
	# Set hotkeys
	#
	if($self->canAdd())
		{
		foreach my $hotkey (@{$self->{'addkeys'}})
			{
		        $tlw->bind ( $hotkey , sub { $butAdd->invoke() } ) ;
			}
		}
	if($self->canChange())
		{
		foreach my $hotkey (@{$self->{'changekeys'}})
			{
		        $tlw->bind ( $hotkey , sub { $butChange->invoke() } ) ;
			}
		}
	if($self->canDelete())
		{
		foreach my $hotkey (@{$self->{'delkeys'}})
			{
		        $tlw->bind ( $hotkey , sub { $butDelete->invoke() } ) ;
			}
		}
	if($self->canPick())
		{
		foreach my $hotkey (@{$self->{'pickkeys'}})
			{
		        $tlw->bind ( $hotkey , sub { $butPick->invoke() } ) ;
			}
		}
	if($self->canEscape())
		{
		foreach my $hotkey (@{$self->{'escapekeys'}})
			{
		        $tlw->bind ( $hotkey , sub { $butEscape->invoke() } ) ;
			}
		}


	########################################
	# SHOW THE WINDOW
	#
	$tlw->focus();
	$list->focus();
	$tlw->grab();
	$tlw->waitWindow();

	########################################
	# RETURN THE CHOICE
	#
	unless ( defined ( $return )  ) { $return = -1; } 
	return ( $return ) ;
	}

1;


Understanding the Picklist Class Code

As an application programmer there's little reason to understand most of the preceding code. For instance, the acquire() method is almost 400 lines of code the application programmer shouldn't need to touch. The showRows()code is a diagnostic routine, and sortRows()simply sorts the picklist based on the end user's clicking of a header. Here are some methods that might be of interest to the application programmer:

addRow() Application programmer uses this call to add a single row to the picklist. The argument is a pointer to an array whose each element is the contents of a field.
formatField() This formats a text string based on the the length and justification properties of a field. An argument is available to tell this method what character to use to fill empty space. For instance, in the ListBox it's usually space, while in the header buttons it's typically a dash.
char2string() Makes a string comprised of the numeric argument length of the character argument character. There's probably a Perl function to do this, but I couldn't find it.
getKeyFromIndex()
Does what it says. Given the index returned by a selected row of the ListBox, it peruses the array of rows for the contents of the field number defined as the key field for the data.
loadPickList() This deletes everything out of the ListBox passed to it and loads the ListBox with the elements of the array reference passed to it.

Beyond these, your main interest will be in the various set and get methods. Some do not contain the words "set" or "get", but are recognizeable as such. Examples include those preceded by enable, disable, can, and is.

For a more complete understanding of the Picklist class source code, see the comments within the source.
Steve Litt is the author of the course on the Universal Troubleshooting Process.  He can be reached at Steve Litt's email address.

Improvements

By Steve Litt
I see three glaring flaws in my Picklist object code:
  1. There's no provision to refresh the list on return from Add, Change or Delete.
  2. The 5 actions are hard coded and there's no provision for programmer defined actions.
  3. Too time consuming and non-intuitive to author.

No provision to refresh the list on return from Add, Change or Delete.

This is pretty simple. Include a reference to the record array in the arguments for the Add, Change and Delete callbacks. Have the callbacks return the key of the record added, changed or deleted. Then have the picklist object reread that one record and refresh the list. To say the least, this isn't rocket science. I just don't have time to do it in the near future.

The 5 actions are hard coded and there's no provision for programmer defined actions.

This is the gravest type of design oversight, and I didn't see it until long after the design and code were complete.

The discovery came as I pondered an app to produce Picklist interface code. The list of fields would be contained in a picklist. Their order would be maintained by a number from 1 to 1000 that gets sorted. But what about reordering fields? Of course one could click a row representing a field, click Edit, and change that number. But change it to what? You need to know the rest of the numbers. From a user interface perspective, there should be an up and a downbutton on the picklist, and these should do the obvious. But given the hard coded nature of the buttons, there's absolutely no way to conveniently do this.

I think version 2 should simply have an array of generic buttons, each equipped with text, hotkeys, and callbacks. Defaults can be set up so the array of buttons is already prepopulated with Add, Change, Delete, Pick and Escape, but the buttons themselves can be add, changed, deleted, or inserted at any point.

Too time consuming and non-intuitive to author

The Picklist is a great interface with a lot of potential, but for real RAD programming the code needs to be constructed programmatically. Probably the best way to do that is to use the PickList object itself, combined with menus and generic form objects.

As discussed in the preceding section, it's unfortunate that my design hardcoded the buttons, because field design would require an up and a down button. Perhaps there's a way of working around that with the current object, or perhaps I can change to an array of buttons (who knows, it might make the Picklist code easier).

Steve Litt is the author of " Troubleshooting Techniques of the Successful Technologist".  Steve can be reached at Steve Litt's email address.

Completion of the Perl/Tk RAD Project

By Steve Litt
To mimic the user interface of Clarion produced programs, I need menus, Picklists and Forms. I believe standard Perl/Tk menus will suffice, and I have the Picklist object. Remaining is a generic or non-generic form object. The form contains fields, and when the user clicks OK or whatever, it performs an action, be it storing the data, or whatever. Some of the field's forms are plain fill-in, and some need to call Picklist objects to select a value for the field. The called Picklist may or may not allow the user to add, change or delete acceptable values. Currently I don't know exactly how to design such a field, but have a feeling it would be a derived widget.

Another remaining challenge is tighter database integration. At the very least, whatever wizards I end up with should be able to read the database, know which tables or views to update, and present the user with lists of tables and columns.

This is a lot of work and I'm not getting to it any time soon. But once I finish it, the 1 day 5  table app will be a reality, and people who get good at this sort of programming can market their skills to a brand new untapped market -- small business.
Steve Litt is the author of " Troubleshooting Techniques of the Successful Technologist".  Steve can be reached at Steve Litt's email address.

Life After Windows: Linux Keeps Rolling On

In the mid and late 1990's we Southeasterners (Florida, Georgia, Alabama, Mississippi, the Carolinas, Virginia, West Virginia, Tennessee and Kentucky) had 2 huge Linux shows every year: Linux Expo in Raleigh, NC, and Atlanta Linux Showcase in Atlanta.

The final Linux Expo occurred in May, 1999. In 2001 Atlanta Linux Showcase became "Annual" Linux Showcase and moved to California. A web search indicates there was no 2002 "Annual" Linux Showcase anywhere. We Southeasterners were disenfranchised.

NOT!

In the vacuum created by the departure of the big shows, LUGs with names like LEAP, JaxLUG and SLUG teamed up with the CTS and ITEC shows to create several outstanding regional Linux presences each year. Linux icons like Jon "maddog" Hall, Robin Miller and Jeremy Allison appeared at some of these Florida regional shows. Florida Linux sparkplug Henry White organized an All Florida Linux Roundtable at the FACUG Statewide Conference, with Roundtable participants Adam Glass, Henry White, Steve Litt and Tony Awtrey.

Speaking of Tony Awtrey, he just organized one of the most successful Florida Linux events in history. Tony's the president of the Melbourne Linux User Group (MLUG) in Melbourne, FL. Tony and some of his fellow LUG members, including his wife Hala Awtrey and master of ceremonies Rob Reilly, managed to bring together Jon "maddog" Hall, SGI's Andrew Fenselau and Carlos Rojas, press member Robin Miller. They lined up sponsors SGI and I.D.E.A.L Technology, enlisted the help of LEAP (Linux Enthusiasts and Professionals on Orlando), procured a wonderful hall at the Hilton Garden Inn in Orlando, and attracted an audience of well over 70 Linux users. The name of the event was "Extreme Linux", and it happened at 7PM on March 7, 2003.

Almost a hundred members filled the audience. Everywhere you looked geeks sported LEAP shirts and MLUG shirts. Fun was had by all as members of the 2 LUGs got to know each other. Also conspicuously in attendance were University of Central Florida students, and Linux geeks from as far away as Venice, Florida. Yet again, even in the most dismal economy in memory, Linux triumphed. We had a great show, and there was even a little bit of swag.

This event absolutely lived up to its "Extreme Linux" name. Jon "maddog" Hall started by stating that programmers really need to understand computers down to the Assembler level in order to take advantage of performance savings. He also mentioned that Linux has the same programming API set from embedded systems clear up to supercomputers. This fact became huge impact during Andrew Fenselau's presentation.

Jon "maddog" Hall's presentation was as technical as you can get without displaying code. He repeatedly pointed out that performance depends on concurrency, and on limiting disk seeks and memory seeks.

His stories included a case where someone sped up a vector multiplication by a factor of 15 simply by inverting one of the vectors, and then inverting the result. Apparently the inverted vector caused the memory cache to stay warm, instead of repeatedly cycling.

Another maddog illustration involved a heavy processing solution he and his co-workers designed. Instead of using the data processing 101 algorithm of read a record, write a record, they created a record ring buffer, and as the disk spun they filled the ring buffer with whatever was at the current head location, and simultaneously (with another processor) ran around the ring buffer processing the data. Also included was an algorithm to grow the ring buffer as more records were read, and shrink the ring buffer as more records were processed.

Maddog made the point that symetrical multiprocessor systems enable a single process to be mapped to a processor, making the processing concurrent without incurring time slicing overhead. He mentioned that when you pipe commands you create separate processes that can use separate processors:
cat | grep this | grep that | sort | myprocess
Every step in that pipeline is a separate process that is assigned a processor, assuming there are enough processors. Because UNIX type processes are so light weight and involve so little overhead, procss startup causes very little latency. So what you basically have is that while myprocess works on record 1, sort works on record 2, grep that works on record 3, grep this works on record 4, and cat works on record 5. Obviously this is a gross simplification, but you get the idea.

Maddog when on to discuss Linux clusters (including Beowulf types). He discussed applications for clusters, and applications which did not suit clusters. He mentioned some limitations of Linux clusters -- network bandwidth, electricity, cooling, floor space and labor. Even if hundreds of people donate their old boxes, sometimes it's not cost effective if the labor involved in configuring all those boxes, load balancing them all, and electrifying all of them exceeds the cost of a naturally more powerful computer.

That set the stage for SGI's Andrew Fenselau to discuss the SGI Altex series of ultra high performance. Andrew began by stating that Beowulf clusters were perfect for many jobs. He went on to say that when network bandwidth, electricity, cooling, square footage, labor and other limitations of traditional Linux clusters made such clusters cost ineffective, the Altex series stepped in to fill the void. The SGI Altex concept involves commodity hardware (Intel Itanium 2) and 64 bit Linux, with various Open Source helper components on top of that and an SGI proprietary layer over that. SGI has solved the bandwidth problem with their Global Shared Memory configuration, in which all the system's memory resides in a single hardware module. All that memory is accessible by any and all processors, which are housed in another module.

The result is a lightning fast machine that can be scaled for processors, memory, I/O, or any combination. This results in huge electricity, air conditioning, and square footage savings, as well as saving labor (SGI's Altex machines are often easier to program than a Beowulf). Furthermore, because it's segmented into a CPU module, a memory module, an i/o module, and a couple other modules, an Altex computer can be scaled in any necessary dimension, without wasting money scaling in all dimensions. Andrew went on to show that because of its commodity components and its Open Source software architecture, Altex gives far better bang for the buck than competing proprietary high performance computers.

Then a LEAPster asked an uncomfortable question. He knew full well that SGI equipment was second to none, but he wondered aloud whether SGI would survive. As Andrew addressed the topic, the audience fidgeted. We'd all asked ourselves the same question.

Then, like a giant oak tree, Jon "maddog" Hall stood and announced we were all asking the wrong question. We needn't inquire about SGI's survival prospects, because Linux presents a single programming API across all systems. At any time we can move to another Linux system and take all our apps with us. Maddog reminded us that our question should be whether SGI's Altex best serves our needs. If the answer is yes, we can confidently proceed forward, knowing that the unified Linux programming API prevents us from being painted into a corner.

It was like a lightning strike, as every audience member simultaneously experienced a mental click.

Behold the power of Linux! No longer must superior technology languish for fear of orphanism. We're now truly free to choose the best technology, knowing when the time comes, we can move forward.

Imagine if this had always been true. Perhaps today we'd all have $400.00 Atari ST machines, running Gem or Linux, capable of creating a fully rendered feature length film by answering a few questions by voice.

Forget retroactively applying this principle to the past and apply it to the future. Imagine the possibilities if companies are rewarded for picking the best technology, irrespective of FUD about the vendor. Imagine being able to purchase an SGI Altex, knowing it will best serve your company, and knowing that making this choice in no way weds you to SGI.

And look at the effect on vendors. A vendor like SGI becomes free to market on the merits of their product, not on their corporate image of success. Oppositely, vendors of inferior software who in the past could coast because people wouldn't buy superior products, would now need to create quality or die.

-*-*-*-

Given the name of this column, I should apply all this to the "Life After Windows" theme. What's life like after Windows?

Once you migrate away from Windows, you join user groups that are fun, informative, and career enhancing. Contrast that to the user group meetings in the Windows world. Now that you're in the Linux camp, you'll find several regional Linux events every year. These are events you can join and work in. Get involved enough and celebrities will call you by your first name.

When you leave Windows behind, you'll find the Linux programming API reduces your labor, and that of your coworkers and underlings. Technology decisions can now be made on technical merit, not on all that corporate "will they last" garbage that brings in technology that made you tear out your hair.

In the past 2 months I've seen evidence that Linux is really starting to win. As Microsoft continues their "all or nothing" monopolistic practices, it's starting to look like increasing numbers are moving to or at least investigating Linux.

I expect Microsoft to turn up the heat in the form of deliberate roadblocks to interoperability, attemped legislation against Open Source, and holding data hostage with proprietary, unexportable file formats. The war will be long and bloody. The longer the war lasts, the worse it will be for Microsoft. If they were to start right now creating compatible, standards based software, they could easily survive. But if they spend the next five years attempting to monopolize, and if that attempt fails, they could perish.

With its single programming API, its openness, and its unpaid developers, Linux is well situated to withstand a 20 year seige. Perhaps next month's column will discuss this in greater detail. But for now, just remember that the Linux users get all the goodies -- API, great geek shows, high performance, low cost (yes, TCO), and probably in the next five years, the majority.

Windows is now starting to falter, but Linux just keeps rolling on.
Life After Windows is a regular Linux Productivity Magazine column, by Steve Litt, bringing you observations and tips subsequent to Troubleshooters.Com's Windows to Linux conversion.
Steve Litt is the author of the course on the Universal Troubleshooting Process.  He can be reached at Steve Litt's email address.

Letters to the Editor

All letters become the property of the publisher (Steve Litt), and may be edited for clarity or brevity. We especially welcome additions, clarifications, corrections or flames from vendors whose products have been reviewed in this magazine. We reserve the right to not publish letters we deem in bad taste (bad language, obscenity, hate, lewd, violence, etc.).
Submit letters to the editor to Steve Litt's email address, and be sure the subject reads "Letter to the Editor". We regret that we cannot return your letter, so please make a copy of it for future reference.

How to Submit an Article

We anticipate two to five articles per issue, with issues coming out monthly. We look for articles that pertain to the Linux or Open Source. This can be done as an essay, with humor, with a case study, or some other literary device. A Troubleshooting poem would be nice. Submissions may mention a specific product, but must be useful without the purchase of that product. Content must greatly overpower advertising. Submissions should be between 250 and 2000 words long.

Any article submitted to Linux Productivity Magazine must be licensed with the Open Publication License, which you can view at http://opencontent.org/openpub/. At your option you may elect the option to prohibit substantive modifications. However, in order to publish your article in Linux Productivity Magazine, you must decline the option to prohibit commercial use, because Linux Productivity Magazine is a commercial publication.

Obviously, you must be the copyright holder and must be legally able to so license the article. We do not currently pay for articles.

Troubleshooters.Com reserves the right to edit any submission for clarity or brevity, within the scope of the Open Publication License. If you elect to prohibit substantive modifications, we may elect to place editors notes outside of your material, or reject the submission, or send it back for modification. Any published article will include a two sentence description of the author, a hypertext link to his or her email, and a phone number if desired. Upon request, we will include a hypertext link, at the end of the magazine issue, to the author's website, providing that website meets the Troubleshooters.Com criteria for links and that the author's website first links to Troubleshooters.Com. Authors: please understand we can't place hyperlinks inside articles. If we did, only the first article would be read, and we can't place every article first.

Submissions should be emailed to Steve Litt's email address, with subject line Article Submission. The first paragraph of your message should read as follows (unless other arrangements are previously made in writing):

Copyright (c) 2003 by <your name>. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, version  Draft v1.0, 8 June 1999 (Available at http://www.troubleshooters.com/openpub04.txt/ (wordwrapped for readability at http://www.troubleshooters.com/openpub04_wrapped.txt). The latest version is presently available at  http://www.opencontent.org/openpub/).

Open Publication License Option A [ is | is not] elected, so this document [may | may not] be modified. Option B is not elected, so this material may be published for commercial purposes.

After that paragraph, write the title, text of the article, and a two sentence description of the author.

Why not Draft v1.0, 8 June 1999 OR LATER

The Open Publication License recommends using the word "or later" to describe the version of the license. That is unacceptable for Troubleshooting Professional Magazine because we do not know the provisions of that newer version, so it makes no sense to commit to it. We all hope later versions will be better, but there's always a chance that leadership will change. We cannot take the chance that the disclaimer of warranty will be dropped in a later version.
 
 

Trademarks

All trademarks are the property of their respective owners. Troubleshooters.Com(R) is a registered trademark of Steve Litt.

URLs Mentioned in this Issue