#!/usr/pkg/bin/perl
#
# Copyright 2007-2014 SPARTA, Inc.  All rights reserved.  See the COPYING
# file distributed with this software for details.
#
# DNSSEC-Tools:  rollrec-editor
#
#	rollrec-editor provides the capability for easy management of rollrec
#	files in a GUI.  New rollrecs may be created and existing rollrecs may
#	be modified or deleted from rollrec-editor.  Only a subset of rollrec
#	fields may be modified by rollrec-editor.
#
#
#	TODO:
#		- undo command
#
#		- quit editor command works, but the exit(0) call generates
#		  a segmentation fault
#

use strict;

use Net::DNS::SEC::Tools::dnssectools;
use Net::DNS::SEC::Tools::conf;
use Net::DNS::SEC::Tools::rollmgr;
use Net::DNS::SEC::Tools::rolllog;
use Net::DNS::SEC::Tools::rollrec;
use Net::DNS::SEC::Tools::BootStrap;

use Getopt::Long qw(:config no_ignore_case_always);

#
# Version information.
#
my $NAME   = "rollrec-editor";
my $VERS   = "$NAME version: 2.1.0";
my $DTVERS = "DNSSEC-Tools Version: 2.2.3";

######################################################################
#
# DESTROY THESE DATA!!!
#
#	These data are part of the undo code that was carried over from
#	signset-editor.  When the undo code is rewritten here, these
#	variables should be either deleted or renamed.
#

my %key_lists;
my @ssnames;
my @krnames;

######################################################################
#
# Detect required Perl modules.
#
dnssec_tools_load_mods(
			'Tk'			=> "",
			'Tk::Dialog'		=> "",
			'Tk::DialogBox'		=> "",
			'Tk::FileSelect'	=> "",
			'Tk::Pane'		=> "",
			'Tk::Table'		=> "",
		      );

#######################################################################
#
#			program (non-GUI) data
#

#
# Variables for command options.
#
my %options = ();				# Filled option array.
my @opts =
(
	"ignore-warns",				# Ignore-edit-warnings flag.
	"no-filter",				# No-name-filtering flag.
	"columns=i",				# Columns in button window.

	"Version",				# Display the version number.
	"help",					# Give a usage message and exit.
);

#
# Flags for options.
#
my $givewarnings = 1;				# Give-warnings flag.
my $namefilter	 = 1;				# Use-name-filter flag (FS).

#
# File name variables.
#
my $rrfile  = "dummy";				# Rollrec file we're examining.
my $curnode = "dummy";				# Node of rrfile.

#
# Lists of rollrecs.
#
my %rollrecs = ();				# Rollrec hash.
my @rollrecs = ();				# Rollrec array.
my @rrnames = ();				# List of rollrec names.
my $numrrnames;					# Number of rollrec names.
my $modified	 = 0;				# Modified-rrfile flag.
my %modified	 = ();				# Modified-rollrecs hash.

###########################################################################
#
# Undo data.
#

my @undo_stack;					# Stack for undo commands.

###########################################################################
#
#			Tk GUI data
#

########################################################
#
# Main window data.
#
my $MAINTITLE	= "DNSSEC-Tools Rollrec Editor";

my $title   = "dummy";				# Node for title.
my $modset  = 0;				# Flag for (modified) title.

#
# The main window and help window.
#
my $wm;						# Main window.
my $helpwin;					# Help window.
my $inhelpwind	 = 0;				# Showing help window flag.

#
# The contents of the main window and its frames.
#
my $mbar;					# Menubar frame.
my $rrfl;					# Keyrec frame.
my $body;					# Window body frame.
my $null;					# Empty frame.

########################################################
#
# Menubar and menu data.
#

#
# Menu item widgets.
#
my $fm_open;					# Open keyrec file item.
my $fm_save;					# Save keyrec file item.
my $fm_svas;					# Save-as keyrec file item.
my $fm_quit;					# Quit file item.

my $em_undo;					# Undo edit item.

my $cm_create;					# Create a rollrec.
my $cm_delete;					# Delete selected rollrecs.
my $cm_modify;					# Modify selected rollrecs.
my $cm_merger;					# Merge two rollrec files.
my $cm_rename;					# Rename selected rollrec.
my $cm_verify;					# Verify a rollrec file.

my $vw_selall;					# Select all rollrecs set item.
my $vw_closeall;				# Close rollrec edit windows.
my $vw_raiser;					# Raise rollrec edit windows.

my $tog_ignwarn;				# Ignore-Edit-Warning toggle.
my $tog_namefilt = 1;				# Use-name-filter toggle.

my $hm_help;					# Help item.

#
# Labels for toggles in the Options menu.
#
my $IGNORE_ON	= "Ignore Edit Warnings";
my $IGNORE_OFF	= "Don't Ignore Edit Warnings";
my $FILTER_OFF	= "Don't Filter Name Selection";
my $FILTER_ON	= "Filter Name Selection";

########################################################
#
# Button window data.
#

#
# Column and row data for button windows.
#
my $DEFCOLS = 4;			# Default columns in button table.
my $MAXCOLS = 12;			# Maximum allowable button columns.
my $MINCOLS = 3;			# Minimum allowable button columns.

my $numcols;				# Current number of table cols.
my $numrows;				# Current number of table rows.

my $columncount = $DEFCOLS;		# Column count.

#
# Font data for text in button window.
#
my $fontsize    = 18;
my $font        = "*-*-bold-r-*-*-$fontsize-*-*-*-*-*-*-*";

#
# Colors for selected/unselected button in button window.
#
my $RRTAB_UNSEL	= 'white';
my $RRTAB_SEL	= 'blue';

########################################################
#
# Edit window data.
#

#
# Row and column constants for the edit-window tables.
#
my $COL_FIELD	 = 0;				# Edit table's label column.
my $COL_DATA	 = 1;				# Edit table's data column.

my $ROW_TYPE	  = 0;				# Type row.
my $ROW_ZONENAME  = 1;				# Zonename row.
my $ROW_ZONEFILE  = 2;				# Zonefile row.
my $ROW_KEYREC	  = 3;				# Keyrec row.
my $ROW_ZONEGROUP = 4;				# Zonegroup row.
my $ROW_ADMIN	  = 5;				# Administrator's email row.
my $ROW_DIR	  = 6;				# Directory row.
my $ROW_DISPLAY	  = 7;				# Display row.
my $ROW_LOG	  = 8;				# Loglevel col.
my $ROW_MAXTTL	  = 9;				# Maxttl row.
my $ROW_ZSARGS	  = 10;				# Zsargs row.

my $ROW_FIRST	 = $ROW_TYPE;			# Edit table's first row.
my $ROW_LAST	 = $ROW_ZSARGS;			# Edit table's last row.

my $ROW_MSG	 = ($ROW_LAST + 1);		# Message line - after ROW_LAST.

#
# Labels for the field-column entries of the edit-window tables.
#
#	These MUST correspond to the $ROW_ constants!
#
my @ET_LABELS =
(
	"Type",
	"Zonename",
	"Zonefile",
	"Keyrec",
	"Zonegroup",
	"Admin Email",
	"Directory",
	"Display Flag",
	"Logging Level",
	"Max-TTL",
	"Zonesigner Arguments",
	"dummy",	# Dummy label, just ensures message row is created.
);

#
# These are the rollrec fields that are editable.
#
my @EDITFIELDS =
		(
			'administrator',
			'directory',
			'display',
			'keyrec',
			'loglevel',
			'maxttl',
			'zonefile',
			'zonegroup',
			'zonename',
			'zsargs'
		);

#
# Constants for message text colors.
#
my $NORMALCOLOR	 = "black";			# Color for normal messages.
my $WARNCOLOR	 = "yellow";			# Color for warning messages.
my $ERRCOLOR	 = "red";			# Color for error messages.

my $rrnametab;					# Rollrec name table widget.

my %editwinds;					# Rollrec edit windows.
my %edittabs = ();				# Edit-area tables.
my %renwinds = ();				# Rollrec rename windows.
my %rentabs  = ();				# Rename something or another.

my %editrbs;			# Type radiobutton values for edit windows.
my %editdfs;			# Display flag radiobutton vals for edit winds.

###########################################################################

main();
exit(0);

#---------------------------------------------------------------------------
# Routine:	main()
#
sub main
{
	my $argc = @ARGV;

	erraction(ERR_EXIT);

	#
	# Check our options.
	#
	doopts();

	#
	# Get the rollrec filename and the path's node.
	#
	$rrfile = $ARGV[0];
	$curnode = getnode($rrfile);
	settitle($curnode);

	#
	# Ensure this rollrec file actually exists.
	#
	if(! -e $rrfile)
	{
		print STDERR "$rrfile does not exist\n";
		exit(1);
	}

	#
	# Build the main window.
	#
	buildmainwind();

	#
	# Start the whole shebang rollin'.
	#
	MainLoop();
}

#-----------------------------------------------------------------------------
# Routine:	doopts()
#
# Purpose:	This routine gets the options from the command line.
#
sub doopts
{
	my $argc = @ARGV;		# Number of command line arguments.

	usage()   if($argc == 0);

	GetOptions(\%options,@opts) || usage();

	#
	# Show the version number or help info if requested.
	#
	version() if(defined($options{'Version'}));
	usage()   if(defined($options{'help'}));

	#
	# Set some flags based on the command line.
	#
	$givewarnings = 0 if(defined($options{'ignore-warns'}));
	$namefilter   = 0 if(defined($options{'no-filter'}));

	#
	# Check for the column-count option.
	#
	if(defined($options{'columns'}))
	{
		$columncount  = $options{'columns'};
		if(($columncount < $MINCOLS) || ($columncount > $MAXCOLS))
		{
			print STDERR "column count must be between $MINCOLS and $MAXCOLS\n";
			exit(1);
		}
	}

}


#---------------------------------------------------------------------------
# Routine:	buildmainwind()
#
# Purpose:	Create and initialize the main window.
#
sub buildmainwind
{
	my $file;					# File menu.
	my $edit;					# Edit menu.
	my $cmds;					# Commands menu.
	my $view;					# Rollrecs menu.
	my $opts;					# Options menu.
	my $help;					# Help menu.

	my $curfile;					# Current keyrec.
	my $nulline;					# Empty line.

	#
	# Create the main window and set its size.
	#
	$wm = MainWindow->new(-title => $MAINTITLE);

	#
	# Get the keyrec file info.  No error message; it was given below.
	#
	exit(1) if(readrrf($rrfile) == 0);

	#
	# Create the frames we'll need.
	#
	$mbar = $wm->Frame(-relief => 'raised', -borderwidth => 1);
	$rrfl = $wm->Frame(-relief => 'raised', -borderwidth => 1);
	$body = $wm->Frame(-relief => 'raised', -borderwidth => 1);
	$null = $wm->Frame(-relief => 'raised', -borderwidth => 1);

	$mbar->pack(-fill => 'x');
	$rrfl->pack(-fill => 'x');
	$body->pack(-fill => 'x');
	$null->pack(-fill => 'x');

	#
	# Create our menus.
	#
	$file = $mbar->Menubutton(-text => 'File',
				  -tearoff => 0,
				  -underline => 0);
	$edit = $mbar->Menubutton(-text => 'Edit',
				  -tearoff => 0,
				  -underline => 0);
	$cmds = $mbar->Menubutton(-text => 'Commands',
				  -tearoff => 0,
				  -underline => 0);
	$view = $mbar->Menubutton(-text => 'View',
				  -tearoff => 0,
				  -underline => 0);
	$opts = $mbar->Menubutton(-text => 'Options',
				  -tearoff => 0,
				  -underline => 0);
	$help = $mbar->Menubutton(-text => 'Help',
				  -tearoff => 0,
				  -underline => 0);

	##################################################
	#
	# Add the File menu entries.
	#
	$fm_open = $file->command(-label => 'Open...',
			          -command => \&file_open,
			          -accelerator => 'Ctrl+O',
			          -underline => 0);
	$fm_save = $file->command(-label => 'Save',
			          -command => \&file_save,
			          -accelerator => 'Ctrl+S',
			          -underline => 0);
	$fm_svas = $file->command(-label => 'Save As...',
			          -command => \&file_saveas,
			          -underline => 0);
	$file->separator();
	$fm_quit = $file->command(-label => 'Quit',
			          -command => \&file_quit,
			          -accelerator => 'Ctrl+Q',
			          -underline => 0);
	$file->pack(-side => 'left');

	$wm->bind('<Control-Key-o>',\&file_open);
	$wm->bind('<Control-Key-s>',\&file_save);
	$wm->bind('<Control-Key-q>',\&file_quit);

	##################################################
	#
	# Add the Edit menu entries.
	#
	$em_undo = $edit->command(-label	=> 'Undo Changes',
				  -command	=> \&edit_undo,
				  -accelerator	=> 'Ctrl+U',
				  -state	=> 'disabled',
				  -underline	=> 0);
	$edit->pack(-side => 'left');

	$wm->bind('<Control-Key-u>',\&edit_undo);

	##################################################
	#
	# Add the Commands menu entries.
	#
	$cm_create = $cmds->command(-label	 => 'New Rollrec...',
				    -command	 => \&cmds_create,
				    -accelerator => 'Ctrl+N',
				    -underline	 => 0);

	$cm_delete = $cmds->command(-label	 => 'Delete Selected Rollrecs',
				    -command	 => \&cmds_delete,
				    -accelerator => 'Ctrl+D',
				    -underline	 => 0);

	$cm_modify = $cmds->command(-label	 => 'Edit Selected Rollrecs...',
				    -command	 => \&cmds_modify,
				    -accelerator => 'Ctrl+E',
				    -underline	 => 0);
	$cmds->separator();

	###################

	$cm_rename = $cmds->command(-label	 => 'Rename Selected Rollrec...',
				    -command	 => \&cmds_rename,
				    -underline	 => 0);

	$cmds->separator();

	###################

	$cm_merger = $cmds->command(-label	 => 'Merge Rollrec Files...',
				    -command	 => \&cmds_merger,
				    -accelerator => 'Ctrl+M',
				    -underline	 => 0);

	$cm_verify = $cmds->command(-label	 => 'Verify Rollrec File',
				    -command	 => [\&cmds_verify, 0],
				    -accelerator => 'Ctrl+V',
				    -underline	 => 0);

	$cm_verify = $cmds->command(-label	 => 'Summarize Problems',
				    -command	 => [\&cmds_verify, 1],
				    -underline	 => 0);

	$cmds->pack(-side => 'left');

	$wm->bind('<Control-Key-n>',\&cmds_create);
	$wm->bind('<Control-Key-d>',\&cmds_delete);
	$wm->bind('<Control-Key-e>',\&cmds_modify);
	$wm->bind('<Control-Key-m>',\&cmds_merger);
	$wm->bind('<Control-Key-v>',\&cmds_verify);

	##################################################
	#
	# Add the Rollrecs menu entries.
	#
	$vw_selall = $view->command(-label => 'Select All Rollrecs',
			            -command => \&view_selall,
			            -accelerator => 'Ctrl+A',
			            -underline => 0);
	$vw_raiser = $view->command(-label => 'Reveal Rollrec Edit Windows',
			            -command => \&view_raiser,
			            -accelerator => 'Ctrl+R',
			            -underline => 0);
	$vw_closeall = $view->command(-label =>'Close Rollrec Edit Windows',
			              -command => \&view_closeall,
			              -accelerator => 'Ctrl+K',
			              -underline => 0);

	$view->pack(-side => 'left');

	$wm->bind('<Control-Key-a>',\&view_selall);
	$wm->bind('<Control-Key-k>',\&view_closeall);
	$wm->bind('<Control-Key-r>',\&view_raiser);

	##################################################
	#
	# Add the Options menu entries.
	#
	$tog_ignwarn = $opts->command(-label => $IGNORE_ON,
			              -command => \&toggle_ignwarn,
			              -underline => 0);
	$opts->pack(-side => 'left');

	$namefilter = 1;
	$tog_namefilt = $opts->command(-label => $FILTER_OFF,
					-command => \&toggle_namefilt,
					-underline => 0);
	$opts->pack(-side => 'left');

	$opts->command(-label => 'Columns in Button Window',
		       -command => [\&set_btncols, 0],
		       -underline => 0);
	$opts->pack(-side => 'left');


	#
	# Set the menu labels based on default values and options.
	#
	$givewarnings = !$givewarnings;
	toggle_ignwarn();

	##################################################
	#
	# Add the Help menu entries.
	#
	$hm_help = $help->command(-label => 'Help',
			          -command => \&help_help,
			          -accelerator => 'Ctrl+H',
			          -underline => 0);
	$help->pack(-side => 'right');

	$wm->bind('<Control-Key-h>',\&help_help);

	##################################################
	#
	# Create a line holding the current rollrec filename.
	#

	$curfile = $rrfl->Label(-text => "Editing Rollrec File:  ");
	$curfile->pack(-side => 'left');
	$curfile = $rrfl->Label(-textvariable => \$title);
	$curfile->pack(-side => 'left');
	$rrfl->pack(-side => 'top', -fill => 'x');

	##################################################
	#
	# Create a table to hold the button window. 
	#
	buildtable(42);

	#
	# Create a line holding the current rollrec filename.
	#
	$nulline = $null->Label(-text => " ");
	$nulline->pack();
	$null->pack(-side => 'top', -fill => 'x');

}

#---------------------------------------------------------------------------
# Routine:	buildtable()
#
# Purpose:	Rebuild the rollrec name table.  This also re-reads the
#		current rollrec file, so the list of rollrec names may
#		increase or shrink depending on the state of that file.
#
sub buildtable
{
	my $readflag = shift;			# Rollrec-read flag.

	my $cnt = 0;				# Count of rollrec names added.

	#
	# Create a brand new table.
	#
	maketable();

	#
	# Read the list of rollrec names.
	#
	readrrf($rrfile,1) if($readflag);
	return if($numrrnames == 0);

	#
	# Ensure the rollrec name list is sorted.
	#
	@rrnames = sort(@rrnames);

	#
	# Re-populate and update the table.
	#
	for(my $ind = 0; $ind < $numrrnames; $ind++)
	{
		my $btn;				# Button widget.

		my $row;				# Cell's row index.
		my $col;				# Cell's column index.

		#
		# Get the column and row indices.
		#
		($col,$row) = ind2cr($ind);

		$btn = $rrnametab->Button(-text => "$rrnames[$ind]",
					  -font => $font,
					  -anchor => 'w',
					  -state  => 'normal',
					  -command => [\&rrname_toggle,
						       $ind],
					  -background => $RRTAB_UNSEL);

		$rrnametab->put($row,$col,$btn);
	}

	$rrnametab->update();

	#
	# Pack it all up.
	#
	$rrnametab->pack(-fill => 'both', -expand => 1);
	$body->pack(-fill => 'both', -expand => 1);
}

#---------------------------------------------------------------------------
# Routine:      rrname_select()
#
# Purpose:      A rollrec name's button has been pushed.  Figure out what
#		to do with it...
#
sub rrname_select
{
	my $rrind = shift;			# Rollrec's name index.

	my $btn;				# Selected button.

	#
	# Get the rollrec's button.
	#
	$btn = getbutton($rrind);

	#
	# ... and set the button's color.
	#
	$btn->configure(-activebackground => $RRTAB_SEL, -background => $RRTAB_SEL);
}

#---------------------------------------------------------------------------
# Routine:      rrname_deselect()
#
# Purpose:      A rollrec name's button has been pushed.  Figure out what
#		to do with it...
#
sub rrname_deselect
{
	my $rrind = shift;			# Rollrec's name index.

	my $btn;				# Selected button.

	#
	# Get the rollrec's button.
	#
	$btn = getbutton($rrind);

	#
	# ... and set the button's color.
	#
	$btn->configure(-activebackground => $RRTAB_UNSEL, -background => $RRTAB_UNSEL);
}

#---------------------------------------------------------------------------
# Routine:      rrname_toggle()
#
# Purpose:      A rollrec name's button has been pushed.  Figure out what
#		to do with it...
#
sub rrname_toggle
{
	my $rrind = shift;			# Rollrec's name index.

	my $rowind;				# Cell's row index.
	my $colind;				# Cell's column index.

	my $btn;				# Selected button.
	my $bgclr;				# New button background color.

	#
	# Get the rollrec's button.
	#
	$btn = getbutton($rrind);

	#
	# Choose an appropriate background color given the button's
	# current state...
	#
	$bgclr = $btn->cget(-background);
	$bgclr = ($bgclr eq $RRTAB_SEL) ? $RRTAB_UNSEL : $RRTAB_SEL;

	#
	# ... and set the button's color.
	#
	$btn->configure(-activebackground => $bgclr, -background => $bgclr);
}

#---------------------------------------------------------------------------
# Routine:      getbutton()
#
# Purpose:      Return a rollrec's button widget given the name's index into
#		@rrnames.
#
sub getbutton
{
	my $rrind = shift;			# Rollrec's name index.

	my $rowind;				# Cell's row index.
	my $colind;				# Cell's column index.

	#
	# Get the column and row indices.
	#
	($colind,$rowind) = ind2cr($rrind);

	#
	# Return the proper button widget.
	#
	return($rrnametab->get($rowind,$colind));
}

#---------------------------------------------------------------------------
# Routine:      selected_rrnames()
#
# Purpose:      Return a list of the selected rollrec names buttons.
#
sub selected_rrnames
{
	my @selected = ();			# Selected rollrec names.

	my $col;				# Column index.
	my $row;				# Row index.
	my $btn;				# Rollrec button.

	for(my $ind=0; $ind < $numrrnames; $ind++)
	{
		($col,$row) = ind2cr($ind);
		$btn = $rrnametab->get($row,$col);

		push @selected, $ind  if($btn->cget(-background) eq $RRTAB_SEL);
	}

	return(sort(@selected));
}

#---------------------------------------------------------------------------
# Routine:      ind2cr()
#
# Purpose:      Convert a rollrec name table index to its table rows and
#		column indices.
#
sub ind2cr
{
	my $nind = shift;				# Rollrec name index.
	my $col;					# Column index.
	my $row;					# Row index.

	$col = int($nind / $numrows);
	$row = $nind % $numrows;

	return($col,$row);
}

#---------------------------------------------------------------------------
# Routine:      maketable()
#
# Purpose:      Create the rollrec name table.
#
sub maketable
{
	#
	# Don't do anything if we don't have any rollrec names.
	#
	return if($numrrnames == 0);

	#
	# Calculate the size of the button table.
	#
	if($numrrnames != 0)
	{
		$numrows = int($numrrnames / $columncount);
		$numrows++ if(($numrrnames % $columncount) != 0);

		$numcols = $columncount;
	}

	#
	# Destroy the rollrec-name table's widgets.
	#
	if($rrnametab)
	{
		$rrnametab->clear;
		$rrnametab->destroy;
	}

	#
	# Create the new button table.
	#
	$rrnametab = $body->Table(-rows		=> $numrows,
				  -columns	=> $numcols,
				  -scrollbars	=> 'e',
				  -relief	=> 'raised',
				  -borderwidth	=> 1,
				  -fixedrows	=> 0,
				  -takefocus	=> 1,
			         );

}

##############################################################################
#
# Menu widget interface routines.
#
##############################################################################

#---------------------------------------------------------------------------
# Routine:	file_open()
#
sub file_open
{
	my $fowin;					# File-open widget.
	my %fsopts = ();				# FileSelect() options.
	my $newfile;					# New file name.

	#
	# Warn the user if this file has been modified.  We'll also give
	# the user a chance to save the file, not save the file, or cancel
	# the process of opening the new file.
	#
	if($modified)
	{
		my $dlg;				# Warning dialog widget.
		my $ret;				# Warning response.

		$dlg = $wm->Dialog(-title => 'Warning',
				   -text  => "$curnode has been modified; save before proceeding?",
				   -buttons => ["Save","Don't Save","Cancel"]);
		$ret = $dlg->Show();

		return if($ret eq "Cancel");

		rollrec_close() if($ret eq "Save");
	}

	#
	# Set up options for FileSelect.
	#
	%fsopts = (-directory => '.');
	$fsopts{'-filter'} = '*.rrf' if($namefilter);

	#
	# Prompt for the new file.  Return to our caller if nothing was chosen.
	#
	$fowin = $wm->FileSelect(%fsopts);
	$newfile = $fowin->Show;
	return if($newfile eq "");

	#
	# Save the new filename and its node.
	#
	$rrfile = $newfile;
	$curnode = getnode($rrfile);
	settitle($curnode);

	#
	# Read the new rollrec file and rebuild the button window.
	#
	if(readrrf($rrfile))
	{
		buildtable(0);
	}
}

#---------------------------------------------------------------------------
# Routine:	file_save()
#
sub file_save
{
	return if(!$modified);

	#
	# Save and re-open the rollrec file.
	#
	rollrec_close();
	readrrf($rrfile);

	#
	# Mark the rollrecs as saved.
	#
	$modified = 0;
	$modset = 0;
	rrsaved();

	#
	# Disable the undo menu option, reset our undo stack, and mark
	# the file as unmodified.
	#
	$em_undo->configure(-state => 'disabled');
	@undo_stack = ();
}

#---------------------------------------------------------------------------
# Routine:	file_saveas()
#
sub file_saveas
{
	my $fowin;					# File-open widget.
	my $newfile;					# New file's name.
	my %fsopts;					# FileSelect() options.

	#
	# Set up options for FileSelect.
	#
	%fsopts = (-directory => '.');
	$fsopts{'-filter'} = '*.rrf' if($namefilter);

	#
	# Prompt for the new file.
	#
	$fowin = $wm->FileSelect(%fsopts);
	$newfile = $fowin->Show;
	return if($newfile eq "");

	#
	# Make sure the user *really* wants to overwrite an existing file.
	# Continue if they do, return if they don't.
	#
	if(-e $newfile)
	{
		my $dlg;				# Warning dialog widget.
		my $ret;				# Warning response.

		$dlg = $wm->Dialog(-title => 'Warning',
				   -text  => "$newfile already exists; overwrite?",
				   -buttons => ["Overwrite","Cancel"]);
		$ret = $dlg->Show();

		return if($ret eq "Cancel");
	}

	#
	# Save the file.
	#
	rollrec_saveas($newfile);
}

#---------------------------------------------------------------------------
# Routine:	file_quit()
#
sub file_quit
{
	#
	# Destroy the rollrec name table's widgets.
	#
	if($rrnametab)
	{
		$rrnametab->clear;
		$rrnametab->destroy;
	}

	#
	# Warn the user if this file has been modified.  We'll also give
	# the user a chance to save the file, not save the file, or cancel
	# the process of opening the new file.
	#
	if($modified)
	{
		my $dlg;				# Warning dialog widget.
		my $ret;				# Warning response.

		$dlg = $wm->Dialog(-title => 'Warning',
				   -text  => "$curnode has been modified; save before proceeding?",
				   -buttons => ["Save","Don't Save","Cancel"]);
		$ret = $dlg->Show();

		return if($ret eq "Cancel");

		rollrec_close() if($ret eq "Save");
	}
	else
	{
		rollrec_discard();
	}

	#
	# Destroy the main window.  This will cause MainLoop() to return,
	# leading to the program exiting.
	#
	$wm->destroy;
}

#---------------------------------------------------------------------------
# Routine:	edit_undo()
#
sub edit_undo
{
	my $undohash;				# First hash from undo stack.

	my $type;				# Undo type.
	my $op;					# Undo operation.
	my $field;				# Undo field.
	my $value;				# Undo value.

	#
	# Ensure we have something to undo.
	#
	return if(@undo_stack == 0);

	#
	# Get the undo entry fields.
	#
	$undohash = shift @undo_stack;
	$type  = $undohash->{'type'};
	$op    = $undohash->{'op'};
	$field = $undohash->{'field'};
	$value = $undohash->{'value'};

	#
	# Undo based on the operation.  This means that creates are deleted,
	# deletes are re-created, and modifies are returned to the old ways.
	#
	if($op eq "create")
	{
		my @signset;			# Signing set's keyrecs.

		#
		# Since we can only create signing sets, that's the only
		# create operation we'll be able to undo.
		#
		if($type eq "signing_set")
		{
			#
			# Delete the signing set name from each keyrec
			# in the set.
			#
			@signset = keyrec_signsets($field);
			foreach my $kr (@signset)
			{
				keyrec_signset_delkey($field,$kr);
			}

			#
			# Delete the signing set name from our list of names.
			#
			for(my $ind=0; $ind < @ssnames; $ind++)
			{
				if($ssnames[$ind] eq $field)
				{
					splice @ssnames, $ind, 1;
					last;
				}
			}
		}

	}
	elsif($op eq "delete")
	{
		my @signset;			# Signing set's keyrecs.

		#
		# If the signing set was deleted, we'll add the signing set
		# to each keyrec in the set.
		# If the keyrec was deleted, we'll add each signing set in
		# the record to the named keyrec.
		#
		if($type eq "signing_set")
		{
			#
			# Add the signing set name to each keyrec in the set.
			#
			@signset = split / /, $value;
			foreach my $kr (sort(@signset))
			{
				keyrec_signset_addkey($field,$kr);
			}

			#
			# Add the new set's name to the list of signing sets.
			#
			push @ssnames, $field;
			@ssnames = sort(@ssnames);
		}
		elsif($type eq "keyrec")
		{
			#
			# Add each signing set name to the keyrec.
			#
			@signset = split / /, $value;

			foreach my $ssn (@signset)
			{
				keyrec_signset_addkey($ssn,$field);
			}

			#
			# Add the keyrec's set list back to the keyrec list.
			#
			$key_lists{$field} = $value;
			push @krnames, $field;
			@krnames = sort(@krnames);
		}

	}
	elsif($op eq "modify")
	{
		#
		# Handle the modify command for signing sets.
		#
		if($type eq "signing_set")
		{
			my @keylist;		# List of keys.
			my $newlist;		# Keys to be replaced.

			#
			# Delete the keys from the signing set.
			#
			$newlist = keyrec_recval($field,'keys');
			@keylist = split / /, $newlist;
			foreach my $key (@keylist)
			{
				keyrec_signset_delkey($field,$key);
			}

			#
			# Restore the old keys to the signing set.
			#
			@keylist = split / /, $value;
			foreach my $key (@keylist)
			{
				keyrec_signset_addkey($field,$key);
			}
		}
		elsif($type eq "keyrec")
		{
			#
			# Handle the modify command for keys.
			#
			my $kk = $field;		# Key we're undoing.

			#
			# Go through all the signing sets to see which need
			# an undo operation.
			#
			foreach my $set (keyrec_signsets())
			{
				my $kfound;		# Key-found flag.
				my $sfound = 0;		# Set-found flag.
				my @undosets = split / /, $value;

				#
				# Figure out if this set holds this key.
				#
				$kfound = keyrec_signset_haskey($set,$kk);

				#
				# Go through the undo set to see if it
				# contains this set.
				#
				foreach my $sset (@undosets)
				{
					if($set eq $sset)
					{
						$sfound = 1;
						last;
					}
				}

				#
				# If the set is in the undo set and it doesn't
				# have the key, add it back in.
				# If the set is not in the undo set and it has
				# the key, take it back out.
				#
				if($sfound)
				{
					if(!$kfound)
					{
						keyrec_signset_addkey($set,$kk);
					}
				}
				else
				{
					if($kfound)
					{
						keyrec_signset_delkey($set,$kk);
					}
				}
			}
		}
	}

	#
	# Disable the undo menu option if this was the last undo item.
	#
	$em_undo->configure(-state => 'disabled') if(@undo_stack == 0);

	#
	# Update the window and decrement our modifications counter.
	#
	$modified--;
	settitle($curnode);
}

#---------------------------------------------------------------------------
# Routine:	undo_add()
#
sub undo_add
{
	my %undo = ();					# Undo hash.

	#
	# Add the arguments to our undo hash.
	#
	$undo{'type'}	= shift;
	$undo{'op'}	= shift;
	$undo{'field'}	= shift;
	$undo{'value'}	= shift;

	#
	# Add the undo hash to the beginning of the undo stack.
	#
	unshift @undo_stack, \%undo;

	#
	# Make sure the undo menu option is enabled.
	#
	$em_undo->configure(-state => 'normal');

	return;
}

#---------------------------------------------------------------------------
# Routine:	cmds_create()
#
sub cmds_create
{
	my $dlg;					# Dialog box.
	my $lab;					# Label widget.
	my $ent;					# Entry widget.
	my $ret;					# Dialog box return.

	my $rrn;					# New rollrec's name.

# print STDERR "cmds_create:  down in New Rollrec\n";

	#
	# Create a new dialog box to get the name of the new rollrec entry.
	#
	$dlg = $wm->DialogBox(-title => 'New Rollrec Name',
			      -buttons => ["Create", "Cancel" ]);

	#
	# Add a description...
	#
	$lab = $dlg->add('Label', -text => 'Enter new rollrec name:  ');
	$lab->pack(-side => 'left');

	#
	# ... and a text entry slot, focus on the entry ...
	#
	$ent = $dlg->add('Entry');
	$ent->pack(-side => 'left');
	$dlg->configure(-focus => $ent);

	#
	# ... mix, stir, and *voila*!  We've got a dialog box.
	#
	$ret = $dlg->Show();

	#
	# Drop out if the user changed their mind.
	#
	return if($ret eq "Cancel");

	#
	# Get the user's requested name.
	#
	$rrn = $ent->get();

	#
	# Give a warning if this rollrec already exists.
	#
	if(rollrec_exists($rrn,'zonefile'))
	{
		my $dlg;			# Warning dialog widget.
		my $ret;			# Warning response.

		$dlg = $wm->Dialog(-title => 'Warning',
				   -text  => "Rollrec $rrn already exists",
				   -buttons => ["Re-enter Name", "Cancel" ]);
		$ret = $dlg->Show();

		#
		# Drop out if the user changed their mind.
		#
		return if($ret eq "Cancel");

		#
		# Let the user try again.
		#
		cmds_create();
	}

	#
	# Set up an edit window for the zone name.
	#
	editwindow($rrn,1);
}

#---------------------------------------------------------------------------
# Routine:	cmds_delete()
#
sub cmds_delete
{
	my @selected = ();			# Selected rollrec indices.
	my @selnames = ();			# Selected rollrec names
	my $numsel;				# Number of selected rollrecs.

	my $ret;				# Warning response.

	my $dlg;				# Warning dialog widget.
	my $nametab;				# Name table.
	my $lab;				# Label for table.
	my $rowind = 0;				# Name table row index.

# print STDERR "cmds_delete:  down in Delete Selected Rollrecs\n";

	#
	# Get the list of selected rollrec indices.
	#
	@selected = selected_rrnames();
	$numsel = @selected;

	#
	# Build the dialog box.
	#
	$dlg = $wm->DialogBox(-title	=> 'Delete Warning',
			      -buttons	=> ["Delete", "Cancel" ]);

	#
	# Add a description.
	#
	$lab = $dlg->add('Label', -text => '   Delete the following rollrecs:');
	$lab->pack(-anchor => 'w', -side => 'top');

	#
	# Build a table to hold the rollrec names.
	#
	$nametab = $dlg->Table(-rows		=> $numsel,
			       -columns		=> 1,
			       -relief		=> 'raised',
			       -scrollbars	=> '',
			       -borderwidth	=> 1,
			       -fixedrows	=> 0,
			       -takefocus	=> 1,
			      );

	#
	# Add the selected rollrec names to the table.
	#
	foreach my $ind (sort {$a <=> $b} @selected)
	{
		my $txt;				# Label text.

		$txt = "    $rrnames[$ind]";

		$lab = $nametab->Label(-text => $txt,-anchor => 'w');
		$lab->pack(-fill => 'x', -side => 'top');

		$nametab->put($rowind,0,$lab);
		$rowind++;
	}

	#
	# Pack the name table.
	#
	$nametab->pack(-side => 'top');

	#
	# Display the dialog box.
	#
	$ret = $dlg->Show();

	#
	# Check for a "don't do this" response.
	#
	return if($ret eq "Cancel");

	#
	# Delete and deselect the selected rollrecs.
	#
	foreach my $rrind (sort {$a <=> $b} @selected)
	{
		rollrec_del($rrnames[$rrind]);
		rrname_deselect($rrind);
	}

	#
	# Delete the selected rollrec names from the list of rollrecs.
	#
	foreach my $rrind (sort {$b <=> $a} @selected)
	{
		splice @rrnames, $rrind, 1;
		$numrrnames--;
	}

	#
	# Rebuild the button table.
	#
	buildtable(0);

	#
	# Set the modified flag.
	#
	$modified++;
	settitle($title);
}

#---------------------------------------------------------------------------
# Routine:	cmds_modify()
#
# Purpose:	This routine creates edit windows for the selected rollrecs.
#
sub cmds_modify
{
	my @selected = ();			# Selected rollrec names.
	my $numsel;				# Number of selected rollrecs.

# print STDERR "cmds_modify:  down in Edit Selected Rollrecs\n";

	#
	# Get the list of selected rollrec names.
	#
	@selected = selected_rrnames();
	$numsel = @selected;

	#
	# Create the edit windows.
	#
	foreach my $rrind (sort(@selected))
	{
		editwindow($rrnames[$rrind],0);
	}
}

#---------------------------------------------------------------------------
# Routine:	cmds_merger()
#
# Purpose:	This routine merges two rollrec files.
#
sub cmds_merger
{
	my $curfile = $rrfile;			# Current rollrec file.
	my $newfile;				# New rollrec file.

	my @curnames = ();			# Rollrec names from cur file.
	my %curnames = ();			# Rollrec name hash -- cur file.
	my @newnames = ();			# Rollrec names from new file.

	my @dupnames = ();			# Duplicated rollrec names.

	my %currrs = ();			# Saved rollrecs from cur file.
	my %newrrs = ();			# Saved rollrecs from new file.

	my $fswin;				# FileSelect window widget.
	my %fsopts;				# Options for FileSelect.
	my $name;				# Rollrec name.
	my $rref;				# Rollrec reference.

# print STDERR "cmds_merger:  down in Merge Rollrec Files\n";

	#
	# Ensure that the currently loaded rollrec matches the version on
	# disk.  If not, don't let the merge happen.
	#
	if($modified)
	{
		errorbox_multi("$curfile has been modified.  Unable to merge until changes have been saved");
		return;
	}

	#
	# Save the rollrec data from the currently open rollrec file.
	#
	@curnames = rollrec_names();
	foreach $name (@curnames)
	{
		$rref = rollrec_fullrec($name);
		$currrs{$name} = $rref;
		$curnames{$name} = 1;
	}

	#
	# Set up options for FileSelect.
	#
	%fsopts = (
			-directory => '.',
			-filelabel => 'File To Be Merged'
		  );
	$fsopts{'-filter'} = '*.rrf' if($namefilter);

	#
	# Get the name of the file to be merged with the current file.
	# We'll call this the "new" file.
	#
	while(42)
	{
		my $cdev;		# Current rollrec's filesys devicenum.
		my $cino;		# Current rollrec's inode number.
		my $ndev;		# New rollrec's filesys devicenum.
		my $nino;		# New rollrec's inode number.

		#
		# Let the user select a new file.
		#
		$fswin = $wm->FileSelect(%fsopts);
		$newfile = $fswin->Show;

		#
		# Return if the user canceled out of the dialog.
		#
		return if($newfile eq '');

		#
		# Get the file info for the current and new rollrec files.
		#
		($cdev,$cino) = stat($curfile);
		($ndev,$nino) = stat($newfile);

		#
		# Drop out of the loop if the two files have different
		# inode numbers or file system device numbers.
		#
		last if(($cino != $nino) || ($cdev != $ndev));

		#
		# Give an error message about the two files.
		#
		errorbox_multi("Cannot merge a rollrec file with itself.\nCurrent rollrec - $curfile\nNew rollrec - $newfile");
	}

	#
	# Ensure the new file is actually a rollrec file.
	#
	if(dt_filetype($newfile) ne "rollrec")
	{
		my $curnode = getnode($newfile);

		errorbox("$curnode is not a rollrec file; unable to continue merge");
		return;
	}

	#
	# Read the new file.  If we couldn't read it, we'll put up a dialog
	# box explaining the problem.
	#
	if(rollrec_read($newfile) < 0)
	{
		my $curnode = getnode($newfile);

		errorbox_multi("Unable to read rollrec file $curnode.  Check to ensure the file exists and is readable.\nComplete path - $newfile");
		return;
	}

	#
	# Save the rollrec data from the new rollrec file.
	#
	@newnames = rollrec_names();
	foreach $name (@newnames)
	{
		$rref = rollrec_fullrec($name);
		$newrrs{$name} = $rref;
	}

	#
	# Check for overlapping names in the two rollrecs.
	#
	foreach $name (@newnames)
	{
		push @dupnames, $name  if(defined($curnames{$name}));
	}

	#
	# Re-read the original file.
	#
	rollrec_close();
	rollrec_read($rrfile);

	#
	# If we've got any duplicate names, we'll report them and let the
	# user decide whether or not to continue.
	#
	if(@dupnames)
	{
		my $ret;				# Dialog's value.

		$ret = dbx_dups(@dupnames);

		return if($ret eq "Cancel");
	}

	#
	# Add the non-overlapping rollrecs to the current rollrec file.
	#
	foreach $name (@newnames)
	{
		my $rref;				# Ref to rollrec.

		#
		# Skip the overlapping rollrecs.
		#
		next if(defined($curnames{$name}));

		#
		# Add rollrec to the in-memory version of the rollrec file.
		#
		$rref = $newrrs{$name};
		rollrec_add($rref->{'rollrec_type'},$name,$rref);
	}

	#
	# Rebuild the button table.
	#
	@rrnames = rollrec_names();
	$numrrnames = @rrnames;
	buildtable(0);

	#
	# Set the modified flag.
	#
	$modified++;
	settitle($title);

}

#---------------------------------------------------------------------------
# Routine:	cmds_rename()
#
# Purpose:	This routine creates the rename rollrec window for the
#		selected rollrec.
#
sub cmds_rename
{
	my @selected = ();			# Selected rollrec names.
	my $numsel;				# Number of selected rollrecs.
	my $rrind;				# Index of selected rollrec.
	my $dbx;				# Error dialog box.

# print STDERR "cmds_rename:  down in Rename Selected Rollrec\n";

	#
	# Get the list of selected rollrec names.
	#
	@selected = selected_rrnames();
	$numsel = @selected;

	#
	# Complain if there wasn't a selected rollrec.
	#
	if($numsel == 0)
	{
		$dbx = $wm->Dialog(-title   => 'Rename Selected Rollrec',
				   -text    => "One rollrec must be selected to use the Rename Rollrec command",
				   -buttons => ["Okay"]);
		$dbx->Show();
		return;
	}

	#
	# Complain if there was more than one selected rollrec.
	#
	if($numsel > 1)
	{
		$dbx = $wm->Dialog(-title   => 'Rename Selected Rollrec',
				   -text    => "A single rollrec must be selected to use the Rename Rollrec command",
				   -buttons => ["Okay"]);
		$dbx->Show();
		return;
	}

	#
	# Let user know that modified rollrecs must be saved prior to the
	# rollrec rename.  If the user is okay with this, we'll save the
	# contents of the rollrec.
	#
	if($modified)
	{
		my $dret;			# Return value from dialog.

		$dbx = $wm->Dialog(-title   => 'Rename Selected Rollrec',
				   -text    => "The rollrec file has been modified.  In order to rename the selected rollrec, the changes will be saved to the rollrec file",
				   -buttons => ["Okay", "Cancel"]);

		$dret = $dbx->Show();
		return if($dret eq 'Cancel');

		file_save();
	}

	#
	# Create the rename-rollrec window.
	#
	$rrind = $selected[0];
	renamewindow($rrnames[$rrind]);
}

#---------------------------------------------------------------------------
# Routine:	cmds_verify()
#
# Purpose:	This routine verifies a rollrec file.  If the summary flag
#		was given, then a dialog box will be created that contains
#		the names and error counts of each bad rollrec.  If the
#		summary flag wasn't given, then an edit window will be
#		put up for each bad rollrec.
#
sub cmds_verify
{
	my $summary = shift;			# Summary-only flag.

	my %errcnts = ();			# Counts of errors.
	my $errcnt  = 0;			# Rollrecs with errors.

	my $dbx;				# Dialog box.
	my $rrtab;				# Bad rollrec table.
	my $lab;				# Label for table.
	my $rowind = 0;				# Error table row index.

	my $errrows;				# Rows in table.
	my $sbars  = 'e';			# Scrollbars.

# print STDERR "cmds_verify:  down in Verify Rollrec File\n";

	#
	# See if any rollrecs have problems.
	#
	foreach my $rrn (reverse(sort(@rrnames)))
	{
		my $errors = 0;			# Errors for rollrec.
		my $warns  = 0;			# Warnings for rollrec.
		my $problems;			# Errs and/or warns for rollrec.

		#
		# Check this rollrec.
		#
		($warns,$errors) = goodrollrec($rrn,0);

		#
		# Get the count of rollrec problems.  If there were any
		# problems, add 'em to the error count hash.
		#
		$problems = $errors;
		$problems += $warns if($givewarnings);
		if($problems)
		{
			$errcnts{$rrn}{'warns'}  = $warns;
			$errcnts{$rrn}{'errors'} = $errors;

			$errcnt++;
		}

		#
		# If there were problems and the user wants more than a
		# summary, we'll put up an edit window for the rollrec
		# and mark up its problem fields.
		#
		if($problems && !$summary)
		{
			editwindow($rrn,0);
			goodrollrec($rrn,1);
		}
	}

	#
	# If there weren't any errors, we'll put up a dialog box saying so
	# and then return once it's dismissed.
	#
	if($errcnt == 0)
	{
		$dbx = $wm->Dialog(-title   => 'Rollrecs Valid',
				   -text    => "Rollrecs passed validity checks",
				   -buttons => ["Done"]);
		$dbx->Show();
		return;
	}

	#
	# Return if the user doesn't want an error summary.
	#
	return if(!$summary);

	#
	# Get the row count, which is the number of erring rollrecs plus
	# a header row.
	# If there's only a few rows, we'll dump the scrollbar.
	#
	$errrows = $errcnt + 1;
	$sbars	= '' if($errrows < 4);

	#
	# Build a dialog box to hold an error summary.
	#
	$dbx = $wm->DialogBox(-title   => 'Rollrecs With Errors',
			      -buttons => ["Done"]);

	#
	# Build a table to hold the rollrec names.
	#
	$rrtab = $dbx->Table(-rows		=> $errrows,
			     -columns		=> 3,
			     -relief		=> 'raised',
			     -scrollbars	=> $sbars,
			     -borderwidth	=> 1,
			     -fixedrows		=> 0,
			     -takefocus		=> 1,
			    );

	#
	# Add some column headers.
	#
	$lab = $rrtab->Label(-text => 'Rollrec');
	$rrtab->put($rowind,0,$lab);
	$lab = $rrtab->Label(-text => 'Warnings');
	$rrtab->put($rowind,1,$lab);
	$lab = $rrtab->Label(-text => 'Errors');
	$rrtab->put($rowind,2,$lab);
	$rowind++;

	#
	# Add the selected rollrec names to the table.
	#
	foreach my $rrn (sort(keys(%errcnts)))
	{
		my $err = $errcnts{$rrn}{'errors'};
		my $wrn = $errcnts{$rrn}{'warns'};

		$lab = $rrtab->Label(-text => $rrn);
		$rrtab->put($rowind,0,$lab);
		$lab = $rrtab->Label(-text => $wrn);
		$rrtab->put($rowind,1,$lab);
		$lab = $rrtab->Label(-text => $err);
		$rrtab->put($rowind,2,$lab);
		$rowind++;
	}

	#
	# Pack the error table.
	#
	$rrtab->pack(-side => 'top');

	#
	# Display the dialog box.
	#
	$dbx->Show();

}

#---------------------------------------------------------------------------
# Routine:	view_selall()
#
# Purpose:	This routine selects/deselects all rollrec names.
#		If any names are selected, the remainder will be selected.
#		If all names are selected, they will all be deselected.
#
sub view_selall
{
	my @selected = ();			# Selected rollrec names.

# print STDERR "view_selall:  down in Select All Rollrecs\n";

	#
	# Get the list of selected rollrec names.
	#
	@selected = selected_rrnames();

	#
	# If all the names are selected, we'll deselect everything.
	# If not all the names are selected, we'll select them all.
	#
	if(@selected == $numrrnames)
	{
		#
		# Unselect all names.
		#
		for(my $ind = 0; $ind < $numrrnames; $ind++)
		{
			rrname_deselect($ind);
		}
		$vw_selall->configure(-label => 'Select All Rollrecs');
	}
	else
	{
		#
		# Select all names.
		#
		for(my $ind=0; $ind < $numrrnames; $ind++)
		{
			my $col;			# Column index.
			my $row;			# Row index.
			my $btn;			# Rollrec button.

			($col,$row) = ind2cr($ind);
			$btn = $rrnametab->get($row,$col);

			rrname_select($ind)  if($btn->cget(-background) eq $RRTAB_UNSEL);
		}
		$vw_selall->configure(-label => 'Unselect All Rollrecs');

	}
}

#---------------------------------------------------------------------------
# Routine:	view_raiser()
#
# Purpose:	This routine brings the rollrec edit windows to the front.
#
sub view_raiser
{

# print STDERR "view_raiser:  down in Raise Rollrec Edit Windows\n";

	#
	# Raise the edit windows.
	#
	foreach my $rrn (reverse(sort(keys(%editwinds))))
	{
		$editwinds{$rrn}->raise;
	}
}

#---------------------------------------------------------------------------
# Routine:	view_closeall()
#
# Purpose:	This routine closes the selected rollrec edit windows.
#		The rollrec's buttons are deselected as well.
#
sub view_closeall
{
# print STDERR "view_closeall:  down in Close All Selected Rollrecs\n";

	#
	# Destroy all edit windows and remove them from the edit-window list.
	#
	foreach my $rrn (sort(keys(%editwinds)))
	{
		editbegone($rrn);
	}
}

#---------------------------------------------------------------------------
# Routine:	view_closeone()
#
# Purpose:	This routine closes the current rollrec edit window.
#		The rollrec's button is deselected as well.
#
sub view_closeone
{
	my $rrn = shift;

# print STDERR "view_closeone:  down in Close Selected Rollrec\n";
	editbegone($rrn);
}

#---------------------------------------------------------------------------
# Routine:	editwindow()
#
# Purpose:	This routine creates a new rollrec-edit window.
#
sub editwindow
{
	my $rrn = shift;			# Name of rollrec to edit.
	my $newflag = shift;			# New-rollrec flag.

	my $editwin;				# Edit window.
	my $eframe;				# Edit frame.
	my $edittab;				# Edit table.
	my $etlen = @ET_LABELS;			# Row-length of table.

	my $frm;				# Frame widget.
	my $lab;				# Label widget.
	my $ent;				# Entry widget.
	my $rb;					# Radiobutton widget.
	my $btn;				# Button widget.
	my $casca;				# Cascading menu widget.

	my $rrec;				# Rollrec reference.
	my $llev;				# Logging level.

# print STDERR "editwindow:  down in Edit Selected Rollrecs\n";

	#
	# If we've already got an edit window for this rollrec, we'll raise
	# that window and return.
	#
	if(defined($editwinds{$rrn}))
	{
		$editwinds{$rrn}->focus;
		return;
	}

	#
	# Get this rollrec's data.
	#
	$rrec = rollrec_fullrec($rrn);

	#
	# If this is a new rollrec, we'll set some defaults.
	#
	if(!rollrec_exists($rrn))
	{
		$rrec->{'rollrec_type'}	 = 'roll';
		$rrec->{'keyrec'}	 = "$rrn.krf";
		$rrec->{'zonefile'}	 = "$rrn.signed";
		$rrec->{'zonename'}	 = "$rrn";
	}

	#
	# Add the rollrec's data to the %rollrecs hash.
	#
	$rollrecs{$rrn}{'administrator'} = $rrec->{'administrator'};
	$rollrecs{$rrn}{'directory'}	 = $rrec->{'directory'};
	$rollrecs{$rrn}{'display'}	 = $rrec->{'display'};
	$rollrecs{$rrn}{'keyrec'}	 = $rrec->{'keyrec'};
	$rollrecs{$rrn}{'loglevel'}	 = $rrec->{'loglevel'};
	$rollrecs{$rrn}{'maxttl'}	 = $rrec->{'maxttl'};
	$rollrecs{$rrn}{'rollrec_type'}	 = $rrec->{'rollrec_type'};
	$rollrecs{$rrn}{'zonefile'}	 = $rrec->{'zonefile'};
	$rollrecs{$rrn}{'zonegroup'}	 = $rrec->{'zonegroup'};

	if(!defined($rrec->{'zonename'}))
	{
		$rollrecs{$rrn}{'zonename'} = $rrn;
	}
	else
	{
		$rollrecs{$rrn}{'zonename'} = $rrec->{'zonename'};
	}

	#
	# Set up the radio buttons' data areas for the record type and
	# the display flag.
	#
	$editrbs{$rrn} = $rollrecs{$rrn}{'rollrec_type'} eq 'roll' ? 1 : 0;
	if(!exists($rollrecs{$rrn}{'display'})	||
	   !defined($rollrecs{$rrn}{'display'}))
	{
		$editdfs{$rrn} = -1;
	}
	elsif($rollrecs{$rrn}{'display'} == 0)
	{
		$editdfs{$rrn} = 0;
	}
	elsif($rollrecs{$rrn}{'display'} == 1)
	{
		$editdfs{$rrn} = 1;
	}

	#
	# Create a new window to hold our edit session.  Bind up some
	# key accelerators, too.
	#
	$editwin = MainWindow->new(-relief	=> 'raised',
				   -title	=> "$rrn",
				   -borderwidth => 1);

	$editwin->bind('<Control-Key-q>', \&file_quit);
	$editwin->bind('<Control-Key-w>', [\&editbegone, $rrn]);
	$editwin->bind('<Control-Key-c>', \&view_closeone);
	$editwin->bind('<Control-Key-k>', \&view_closeall);

	#
	# Now make the containers for the window.
	#
	$eframe = $editwin->Frame(-relief => 'raised', -borderwidth => 1);
	$eframe->pack(-fill => 'x');

	#
	# Create a table to hold the rollrec data.
	#
	$edittab = $eframe->Table(-rows		=> $etlen,
				  -columns	=> 2,
				  -relief	=> 'raised',
				  -scrollbars	=> '',
				  -borderwidth	=> 1,
				  -fixedrows	=> 0,
				  -takefocus	=> 1,
			         );

	#----------------
	#
	# Set the field column.
	#
	for(my $ind = $ROW_FIRST; $ind <= $ROW_LAST; $ind++)
	{
		$lab = $eframe->Label(-text => $ET_LABELS[$ind],-anchor => 'w');
		$lab->pack(-fill => 'x', -side => 'top');
		$edittab->put($ind,$COL_FIELD,$lab);
	}

	#----------------
	#
	# Record type:  Add the stuff for the record type.
	#
	$frm = $eframe->Frame(-relief => 'raised', -borderwidth => 1);
	$rb = $frm->Radiobutton(-text	   => 'Roll',
				-variable => \$editrbs{$rrn},
				-value	   => 1);
	$rb->pack(-fill => 'x', -side => 'left');
	$rb = $frm->Radiobutton(-text	   => 'Skip',
				-variable => \$editrbs{$rrn},
				-value	   => 0);
	$rb->pack(-fill => 'x', -side => 'left');
	$frm->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_TYPE,$COL_DATA,$frm);

	#----------------
	#
	# Zonename:  Add the stuff for the rollrec's zonename.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'zonename'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_ZONENAME,$COL_DATA,$ent);

	#----------------
	#
	# Zonefile:  Add the stuff for the rollrec's zonefile.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'zonefile'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_ZONEFILE,$COL_DATA,$ent);

	#----------------
	#
	# Keyrec:  Add the stuff for the rollrec's keyrec.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'keyrec'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_KEYREC,$COL_DATA,$ent);

	#----------------
	#
	# Zonegroup:  Add the stuff for the rollrec's zonegroup.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'zonegroup'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_ZONEGROUP,$COL_DATA,$ent);

	#----------------
	#
	# Administrator:  Add the stuff for the rollrec's admin mail.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'administrator'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_ADMIN,$COL_DATA,$ent);

	#----------------
	#
	# Directory:  Add the stuff for the rollrec's directory.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'directory'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_DIR,$COL_DATA,$ent);

	#----------------
	#
	# Display:  Add the stuff for the rollrec's display flag.
	#
	$frm = $eframe->Frame(-relief => 'raised', -borderwidth => 1);
	$rb = $frm->Radiobutton(-text	   => 'On',
				-variable => \$editdfs{$rrn},
				-value	   => 1);
	$rb->pack(-fill => 'x', -side => 'left');
	$rb = $frm->Radiobutton(-text	   => 'Off',
				-variable => \$editdfs{$rrn},
				-value	   => 0);
	$rb->pack(-fill => 'x', -side => 'left');
	$rb = $frm->Radiobutton(-text	   => 'Default',
				-variable => \$editdfs{$rrn},
				-value	   => -1);
	$rb->pack(-fill => 'x', -side => 'left');
	$frm->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_DISPLAY,$COL_DATA,$frm);

	#----------------
	#
	# Logging level:  Add the stuff for the rollrec's log level.
	#

	#
	# Get the label for the logging level's menu button.  If it's defined
	# in the rollrec, we'll make sure we have the string value.  If we
	# can't convert it, we'll use a dummy error value.  If it isn't
	# defined in the rollrec, we'll use "default" for the button label.
	#
	if(defined($rollrecs{$rrn}{'loglevel'}))
	{
		$llev = rolllog_str($rollrecs{$rrn}{'loglevel'});
		if($llev eq '')
		{
			$llev = '<bad value in rollrec file>';
		}
	}
	else
	{
		$llev = 'default';
	}

	#
	# Create the menu button.
	#
	$ent = $eframe->Menubutton(-text    => $llev,
				   -tearoff => 0);

	#
	# Add a new radiobutton for each logging level.
	#
	$casca = $ent->cascade(-label => '');
	foreach $llev (rolllog_levels())
	{
		$casca->radiobutton(-label    => "$llev",
				    -variable => \$rollrecs{$rrn}{'loglevel'},
				    -command  => [\&setloglvl, $ent, $llev]);
	}

	#
	# Add a default entry.
	#
	$casca->radiobutton(-label    => "default",
			    -variable => \$rollrecs{$rrn}{'loglevel'},
			    -command  => [\&setloglvl, $ent, 'default']);

	#
	# Make it all available to the user.
	#
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_LOG,$COL_DATA,$ent);

	#----------------
	#
	# Max-TTL:  Add the stuff for the rollrec's maximum TTL.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'maxttl'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_MAXTTL,$COL_DATA,$ent);

	#----------------
	#
	# ZSArgs:  Add the stuff for the rollrec's zonesigner arguments.
	#
	$ent = $eframe->Entry(-textvariable => \$rollrecs{$rrn}{'zsargs'});
	$ent->pack(-fill => 'x', -side => 'top');
	$edittab->put($ROW_ZSARGS,$COL_DATA,$ent);

	#----------------
	#
	# Done adding our data.  Do another pack, just for kicks.
	#
	$edittab->pack(-side => 'top');

	#
	# Add a button to dismiss the window.
	#
	$btn = $eframe->Button(-text	 => 'Save',
			       -command => [\&editsaver, $rrn, $newflag]);
	$btn->pack(-side => 'left', -fill => 'x', -expand => 1);
	$btn = $eframe->Button(-text	 => 'Cancel',
			       -command => [\&editbegone, $rrn]);
	$btn->pack(-side => 'left', -fill => 'x', -expand => 1);

	#
	# Save the new edit window in the hash table of edit windows.
	#
	$editwinds{$rrn} = $editwin;
	$edittabs{$rrn}  = $edittab;

}

#---------------------------------------------------------------------------
# Routine:	renamewindow()
#
# Purpose:	This routine creates a new rollrec-rename window.
#
sub renamewindow
{
	my $rrn = shift;			# Name of rollrec to rename.
	my $dbx;				# Error dialog box.

	my $renwin;				# Rename window.

	##########

	my $rframe;				# Rename frame.
	my $rentab;				# Rename table.

	my $frm;				# Frame widget.
	my $lab;				# Label widget.
	my $ent;				# Entry widget.
	my $btn;				# Button widget.

# print STDERR "renamewindow:  down in Rename Rollrec\n";

	#
	# If there's a rename window for this rollrec, we'll give an error
	# and return.
	#
	if(defined($renwinds{$rrn}))
	{
		$dbx = $wm->Dialog(-title   => 'Rename Selected Rollrec',
				   -text    => "This rollrec is already being renamed",
				   -buttons => ["Okay"]);
		$dbx->Show();
		return;
	}

	#
	# If there's an edit window for this rollrec, we'll give an error
	# and return.
	#
	if(defined($editwinds{$rrn}))
	{
		$dbx = $wm->Dialog(-title   => 'Rename Selected Rollrec',
				   -text    => "Unable to rename a rollrec while it is being edited",
				   -buttons => ["Okay"]);
		$dbx->Show();
		return;
	}

	#
	# Create a new window to hold our rename session.  Bind up some
	# key accelerators, too.
	#
	$renwin = MainWindow->new(-relief	=> 'raised',
				   -title	=> "Rename $rrn",
				   -borderwidth => 1);

	$renwin->bind('<Control-Key-q>', \&file_quit);
	$renwin->bind('<Control-Key-w>', [\&renamebegone, $rrn]);

	#
	# Now make the containers for the window.
	#
	$rframe = $renwin->Frame(-relief => 'raised', -borderwidth => 1);
	$rframe->pack(-fill => 'x');

	#
	# Create a table to hold the rollrec data.
	#
	$rentab = $rframe->Table(-rows		=> 2,
				 -columns	=> 2,
				 -relief	=> 'raised',
				 -scrollbars	=> '',
				 -borderwidth	=> 1,
				 -fixedrows	=> 0,
				 -takefocus	=> 1,
			        );

	#
	# Add the label column.
	#
	$lab = $rframe->Label(-text => 'New rollrec name',-anchor => 'w');
	$lab->pack(-fill => 'x', -side => 'top');
	$rentab->put(0,0,$lab);

	#
	# Add the empty field for the rollrec's new name.
	#
	$rollrecs{$rrn}{'newname'} = '';
	$ent = $rframe->Entry(-textvariable => \$rollrecs{$rrn}{'newname'});
	$ent->pack(-fill => 'x', -side => 'top');
	$rentab->put(0,1,$ent);

	#
	# Done adding our data.  Do another pack, just for kicks.
	#
	$rentab->pack(-side => 'top');

	#
	# Add a button to dismiss the window.
	#
	$btn = $rframe->Button(-text	 => 'Rename',
			       -command => [\&renamesaver, $rrn]);
	$btn->pack(-side => 'left', -fill => 'x', -expand => 1);
	$btn = $rframe->Button(-text	 => 'Cancel',
			       -command => [\&renamebegone, $rrn]);
	$btn->pack(-side => 'left', -fill => 'x', -expand => 1);

	#
	# Save the new rename window in the hash table of rename windows.
	#
	$renwinds{$rrn} = $renwin;
	$rentabs{$rrn}  = $rentab;
}

#---------------------------------------------------------------------------
# Routine:	setloglvl()
#
# Purpose:	Set the logging level label in an edit window's menu button.
#
sub setloglvl
{
	my $ent = shift;				# Menu label to change.
	my $llev = shift;				# New menu label.

	$ent->configure(-text => $llev);
}

##############################################################################
#
# Utility routines
#
##############################################################################

#---------------------------------------------------------------------------
# Routine:	readrrf()
#
sub readrrf
{
	my $rrf = shift;				# Rollrec file.
	my @names;					# Rollrec names.

	#
	# If the specified file doesn't exist, ask the user if we should
	# continue or quit.
	#
	if(! -e $rrf)
	{
		my $dlg;			# Warning dialog widget.
		my $ret;			# Warning response.

		$dlg = $wm->Dialog(-title => 'Warning',
				   -text  => "$curnode does not exist",
				   -buttons => ["Continue", "Quit" ]);
		$ret = $dlg->Show();

		return(1) if($ret eq "Continue");

		file_quit();
	}

	#
	# Ensure the file is actually a rollrec file.
	#
	if(dt_filetype($rrf) ne "rollrec")
	{
		my $curnode = getnode($rrf);

		errorbox("$curnode is not a rollrec file; unable to continue");
		return(0);
	}

	#
	# Zap the old data.
	#
	@rrnames = ();
	@rollrecs = ();

	#
	# Get data from the rollrec file.
	#
	rollrec_read($rrf);
	@rrnames = rollrec_names();
	$numrrnames = @rrnames;

	#
	# Initialize the rollrecs hash for each rollrec.
	#
	foreach my $rrn (@rrnames)
	{
		my $rrec;				# This rollrec's data.

		#
		# Get this rollrec's data.
		#
		$rrec = rollrec_fullrec($rrn);

		#
		# Add the rollrec's data to the %rollrecs hash.
		#
		$rollrecs{$rrn}{'administrator'} = $rrec->{'administrator'};
		$rollrecs{$rrn}{'directory'}	 = $rrec->{'directory'};
		$rollrecs{$rrn}{'display'}	 = $rrec->{'display'};
		$rollrecs{$rrn}{'keyrec'}	 = $rrec->{'keyrec'};
		$rollrecs{$rrn}{'loglevel'}	 = $rrec->{'loglevel'};
		$rollrecs{$rrn}{'maxttl'}	 = $rrec->{'maxttl'};
		$rollrecs{$rrn}{'rollrec_type'}	 = $rrec->{'rollrec_type'};
		$rollrecs{$rrn}{'zonefile'}	 = $rrec->{'zonefile'};
		$rollrecs{$rrn}{'zonegroup'}	 = $rrec->{'zonegroup'};
		$rollrecs{$rrn}{'zonename'}	 = $rrec->{'zonename'};
		$rollrecs{$rrn}{'zsargs'}	 = $rrec->{'zsargs'};

		#
		# Set up the radio buttons' data areas for the record type and
		# the display flag.
		#
		$editrbs{$rrn} = $rollrecs{$rrn}{'rollrec_type'} eq 'roll' ? 1 : 0;
		if(!exists($rollrecs{$rrn}{'display'})	||
		   !defined($rollrecs{$rrn}{'display'}))
		{
			$editdfs{$rrn} = -1;
		}
		elsif($rollrecs{$rrn}{'display'} == 0)
		{
			$editdfs{$rrn} = 0;
		}
		elsif($rollrecs{$rrn}{'display'} == 1)
		{
			$editdfs{$rrn} = 1;
		}
	}

	#
	# Reset the modified-keyrec file flag.
	#
	$modified = 0;
	settitle($curnode);
	return(1);
}

#---------------------------------------------------------------------------
# Routine:	getnode()
#
# Purpose:	Get the node of a pathname.
#
sub getnode
{
	my $path = shift;				# Path to nodify.

	my @pathelts;					# Path elements.
	my $pathnode;					# Last path elements.

	@pathelts = split /\//, $path;
	$pathnode = pop @pathelts;

	return($pathnode);
}

#---------------------------------------------------------------------------
# Routine:	editsaver()
#
# Purpose:	Save a rollrec and get rid of its edit window.
#
sub editsaver
{
	my $rrn = shift;			# Name of rollrec to save.
	my $newflag = shift;			# New-rollrec flag.

# print STDERR "editsaver:  saver rollrec edits for $rrn\n";

	$rollrecs{$rrn}{'display'} = $editdfs{$rrn};

	#
	# If this is a valid rollrec, we'll get rid of the edit window
	# and save the data.  If this is a new rollrec, we'll also add
	# a rollrec button to the main window.
	#
	if(goodrollrec($rrn,1))
	{
		#
		# If this is a new rollrec, add the name to the rollrec
		# list and then rebuild the main button table.
		#
		if($newflag)
		{
			push @rrnames, $rrn;
			$numrrnames++;
			buildtable(0);
		}

		#
		# Get rid of the edit window and save the rollrec data.
		#
		editbegone($rrn);
		saverollrec($rrn);
	}

}

#---------------------------------------------------------------------------
# Routine:	renamesaver()
#
# Purpose:	Save a rollrec and get rid of its rename window.
#
sub renamesaver
{
	my $rrn = shift;			# Name of rollrec to save.
	my $newname;				# New rollrec name.
	my $newname2;				# Copy of new rollrec name.

# print STDERR "renamesaver:  rename rollrec edits for $rrn\n";

	$newname = $rollrecs{$rrn}{'newname'};

	#
	# Ensure a new name was given.
	#
	if($newname eq '')
	{
		errorbox("A new rollrec name must be given");
		delete $renwinds{$rrn};
		delete $rentabs{$rrn};
		return;
	}

	#
	# Ensure a *valid* new name was given.
	#
	$newname2 = $newname;
	$newname2 =~ s/[a-zA-Z0-9\/\-+_.,: \@\t]//g;
	if(length($newname2) > 0)
	{
		errorbox("\"$newname\" is an invalid rollrec name");
		delete $renwinds{$rrn};
		delete $rentabs{$rrn};
		return;
	}

	#
	# Rename the rollrec.
	#
	rollrec_rename($rrn,$newname);
	$modified = 1;

	#
	# Write the rollrec file contents and rebuild our rollrec list.
	#
	rollrec_close();
	buildtable(1);

	#
	# Get rid of the rename window.
	#
	renamebegone($rrn,$newname);
	$modified = 0;
}

#-----------------------------------------------------------------------------
# Routine:	goodrollrec()
#
# Purpose:	Ensure that the rollrec has valid data.
#
#		The following fields may be empty:
#			- administrator
#			- directory
#			- display
#			- loglevel
#			- maxttl
#			- zonegroup
#
#		Error checks are:
#			- empty zonename field
#			- invalid logging level
#			- non-positive maxttl
#			- invalid display value
#			- zone file is not a regular file
#			- keyrec file is not a regular file
#
#		Warning checks are:
#			- zone directory doesn't exist
#			- zone directory isn't a directory
#			- zone file doesn't exist
#			- zone file isn't a regular file
#			- keyrec file doesn't exist
#			- keyrec file isn't a regular file
#
sub goodrollrec
{
	my $rrn = shift;			# Name of the rollrec.
	my $upd = shift;			# Update-window flag.

# print STDERR "goodrollrec:  down in  ($rrn)\n";

	my $errors = 0;				# Error count.
	my $warns = 0;				# Warning count.
	my $problems = 0;			# Problem count.

	my $admin;				# Administrator email address.
	my $dispflag;				# Display flag.
	my $krfile;				# Keyrec file.
	my $loglevel;				# Logging level.
	my $maxttl;				# Maximum TTL.
	my $zonedir;				# Directory.
	my $zonefile;				# Zonefile.
	my $zonegroup;				# Zonegroup.
	my $zonename;				# Zonename.
	my $zsargs;				# Zonesigner arguments.

	#
	# Reset the message fields.
	#
	if($upd)
	{
		addmsg($rrn,$COL_FIELD,0,' ');
		addmsg($rrn,$COL_DATA,0,' ');
	}

	#
	# Strip all whitespace from the beginning and end of the fields.
	#
	foreach my $fld (@EDITFIELDS)
	{
		next if(!exists($rollrecs{$rrn}{$fld}) ||
			($rollrecs{$rrn}{$fld} eq ''));
		$rollrecs{$rrn}{$fld} =~ s/^[ \t]*//;
		$rollrecs{$rrn}{$fld} =~ s/[ \t]*$//;
	}

	#
	# Get the rollrec data.
	#
	$admin	   = $rollrecs{$rrn}{'administrator'};
	$dispflag  = $rollrecs{$rrn}{'display'};
	$krfile	   = $rollrecs{$rrn}{'keyrec'};
	$loglevel  = $rollrecs{$rrn}{'loglevel'};
	$maxttl	   = $rollrecs{$rrn}{'maxttl'};
	$zonedir   = $rollrecs{$rrn}{'directory'};
	$zonefile  = $rollrecs{$rrn}{'zonefile'};
	$zonegroup = $rollrecs{$rrn}{'zonegroup'};
	$zsargs	   = $rollrecs{$rrn}{'zsargs'};
	if(!defined($rollrecs{$rrn}{'zonename'}))
	{
		$zonename = $rrn;
	}
	else
	{
		$zonename = $rollrecs{$rrn}{'zonename'};
	}

	#
	# Reset all the label colors.
	#
	if($upd)
	{
		fieldcolor($ROW_ZONEFILE,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_KEYREC,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_ZONEGROUP,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_ADMIN,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_DIR,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_DISPLAY,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_LOG,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_MAXTTL,$rrn,$NORMALCOLOR);
		fieldcolor($ROW_ZSARGS,$rrn,$NORMALCOLOR);
	}

	#
	# Check the zonename.
	#
	if($zonename eq '')
	{
		errval($ROW_ZONENAME,$rrn,"no zone name specified") if($upd);
		$errors++;
	}

	#
	# Check the zone file.
	#
	if($zonefile eq '')
	{
		errval($ROW_ZONEFILE,$rrn,"no zone file specified") if($upd);
		$errors++;
	}
	else
	{
		if((!-e $zonefile) && $givewarnings)
		{
			warnval($ROW_ZONEFILE,$rrn,"zone file \"$zonefile\" does not exist") if($upd);
			$warns++;
		}
		elsif((!-f $zonefile) && $givewarnings)
		{
			warnval($ROW_ZONEFILE,$rrn,"zone file \"$zonefile\" is not a regular file") if($upd);
			$warns++;
		}
	}

	#
	# Check the keyrec file.
	#
	if($krfile eq '')
	{
		errval($ROW_KEYREC,$rrn,"no keyrec file specified") if($upd);
		$errors++;
	}
	else
	{
		if((!-e $krfile) && $givewarnings)
		{
			warnval($ROW_KEYREC,$rrn,"keyrec file \"$krfile\" does not exist") if($upd);
			$warns++;
		}
		elsif((!-f $krfile) && $givewarnings)
		{
			warnval($ROW_KEYREC,$rrn,"keyrec file \"$krfile\" is not a regular file") if($upd);
			$warns++;
		}
	}

	#
	# No validity checks for the zonegroup.
	#

	#
	# No validity checks for the administrator.
	#

	#
	# Check the directory.
	#
	if(exists($rollrecs{$rrn}{'directory'}))
	{
		if(($rollrecs{$rrn}{'directory'}) ne '')
		{
			if(!-e $zonedir)
			{
				warnval($ROW_DIR,$rrn,"directory \"$zonedir\" does not exist") if($upd);
				$warns++;
			}
			elsif(!-d $zonedir)
			{
				warnval($ROW_DIR,$rrn,"directory \"$zonedir\" is not a directory") if($upd);
				$warns++;
			}
		}
	}

	#
	# Make sure we've got a valid display flag.
	#
	if(defined($dispflag))
	{
		if(($dispflag != 0) && ($dispflag != 1) && ($dispflag != -1))
		{
			errval($ROW_DISPLAY,$rrn,"invalid display flag \"$dispflag\"") if($upd);
			$errors++;
		}
	}

	#
	# Make sure we've got a valid logging level.
	#
	if(exists($rollrecs{$rrn}{'loglevel'}))
	{
		if(($rollrecs{$rrn}{'loglevel'} ne '') &&
		   ($rollrecs{$rrn}{'loglevel'} ne 'default'))
		{
			if(rolllog_level($loglevel,0) < 0)
			{
				errval($ROW_LOG,$rrn,"invalid loglevel \"$loglevel\"") if($upd);
				$errors++;
			}
		}
	}

	#
	# Make sure we've got a valid maximum TTL.
	#
	if(exists($rollrecs{$rrn}{'maxttl'}))
	{
		if($rollrecs{$rrn}{'maxttl'} ne '')
		{
			if($maxttl < 1)
			{
				errval($ROW_MAXTTL,$rrn,"invalid maxttl \"$maxttl\"") if($upd);
				$errors++;
			}
		}
	}

	#
	# No error checks for zonesigner arguments.
	#

	#
	# If we found any errors or warnings, we'll add a message about them.
	#
	if($upd)
	{
		addmsg($rrn,$COL_FIELD,$errors,"error") if($errors);
		addmsg($rrn,$COL_DATA,$warns,"warn") if($warns);
	}

	#
	# Add the warning count to the error count if we shouldn't ignore
	# warnings
	#
	$problems  = $errors;
	$problems += $warns if($givewarnings);

	#
	# If we've found this rollrec is good, we'll make some adjustments.
	#
	if(!$errors)
	{
		#
		# Delete the administrator if it's empty.
		#
		if($rollrecs{$rrn}{'administrator'} eq '')
		{
			delete $rollrecs{$rrn}{'administrator'};
		}

		#
		# Delete the directory if it's empty.
		#
		if(($rollrecs{$rrn}{'directory'}) eq '')
		{
			delete $rollrecs{$rrn}{'directory'};
		}

		#
		# Delete the loglevel if it's empty or the default was chosen.
		#
		if(($rollrecs{$rrn}{'loglevel'} eq '')	||
		   ($rollrecs{$rrn}{'loglevel'} eq 'default'))
		{
			delete $rollrecs{$rrn}{'loglevel'};
		}

		#
		# Delete the max-ttl if it's empty.
		#
		if($rollrecs{$rrn}{'maxttl'} eq '')
		{
			delete $rollrecs{$rrn}{'maxttl'};
		}

		#
		# Delete the zonegroup if it's empty.
		#
		if(($rollrecs{$rrn}{'zonegroup'}) eq '')
		{
			delete $rollrecs{$rrn}{'zonegroup'};
		}

		#
		# Delete the zsargs if it's empty.
		#
		if($rollrecs{$rrn}{'zsargs'} eq '')
		{
			delete $rollrecs{$rrn}{'zsargs'};
		}

	}

	return($warns,$errors) if(!$upd);
	return(!$problems);
}

#-----------------------------------------------------------------------------
# Routine:	saverollrec()
#
# Purpose:	Save the rollrec's data into the rollrec module data.
#
#
sub saverollrec
{
	my $rrn = shift;			# Name of the rollrec.

	my $typestr;				# Calculated rollrec type.

# print STDERR "saverollrec:  down in  ($rrn)\n";

	#
	# Get the rollrec type from %editrbs.
	#
	$typestr = $editrbs{$rrn} ? "roll" : "skip";

	#
	# Get the display flag from %editdfs.
	#
	if(!exists($editdfs{$rrn}) || !defined($editdfs{$rrn}) ||
	   ($editdfs{$rrn} == -1))
	{
		delete($rollrecs{$rrn}{'display'});
	}
	elsif($editdfs{$rrn} == 0)
	{
		$rollrecs{$rrn}{'display'} = 0;
	}
	elsif($editdfs{$rrn} == 1)
	{
		$rollrecs{$rrn}{'display'} = 1;
	}

	#
	# Set the rollrec's type.
	#
	rollrec_rectype($rrn,$typestr);

	#
	# Save the fields into the rollrec itself.  If the field exists
	# in %rollrecs{$rrn}, we'll save the value.  If the field does
	#  not exist, we'll delete the field.
	#
	foreach my $field (@EDITFIELDS)
	{
		if(exists($rollrecs{$rrn}{$field}))
		{
			rollrec_setval($rrn,$field,$rollrecs{$rrn}{$field});
		}
		else
		{
			rollrec_delfield($rrn,$field);
		}
	}

	#
	# Mark this rollrec as having been modified.
	#
	rrmodified($rrn);
}

#---------------------------------------------------------------------------
# Routine:	rrmodified()
#
# Purpose:	Mark a rollrec as having been modified.
#
sub rrmodified
{
	my $rrn = shift;				# Modified rollrec.

	#
	# Increment the modified-keyrec flag.
	#
	$modified++;
	$modified{$rrn} = 1;

	settitle($title);

	#
	# Re-populate and update the table.
	#
	for(my $ind = 0; $ind < $numrrnames; $ind++)
	{
		my $row;				# Cell's row index.
		my $col;				# Cell's column index.

		my $btn;				# Button widget.
		my $name;				# Label of button.

		#
		# Get the column and row indices.
		#
		($col,$row) = ind2cr($ind);

		#
		# Get this button's label.
		#
		$btn = $rrnametab->get($row,$col);
		$name = $btn->cget(-text);

		#
		# Modify the name of just the button we're looking for.
		#
		next if($name ne $rrn);
		$btn->configure(-text => "* $name");
	}

}

#---------------------------------------------------------------------------
# Routine:	rrsaved()
#
# Purpose:	Mark the rollrecs as having been saved.
#
sub rrsaved
{
	#
	# Re-populate and update the table.
	#
# print STDERR "rrsaved:  numrrnames - $numrrnames\n";
	for(my $ind = 0; $ind < $numrrnames; $ind++)
	{
		my $row;				# Cell's row index.
		my $col;				# Cell's column index.

		my $btn;				# Button widget.
		my $name;				# Label of button.

		#
		# Get the column and row indices.
		#
		($col,$row) = ind2cr($ind);

		#
		# Get this button's label.
		#
		$btn = $rrnametab->get($row,$col);
		$name = $btn->cget(-text);
		$name =~ s/^\* //;

		#
		# Skip buttons that haven't been modified.
		#
		next if(!$modified{$name});

		#
		# Modify the the button's label.
		#
		$btn->configure(-text => $name);
	}

	#
	# Reset the modified-keyrec flag.
	#
	$modified = 0;
	%modified = ();

	settitle($title);
}

#---------------------------------------------------------------------------
# Routine:	errval()
#
# Purpose:	Take an error action.
#
sub errval
{
	my $row = shift;				# Errant rollrec's row.
	my $rrn = shift;				# Errant rollrec's name.
	my $msg = shift;				# Error message.

#	print STDERR "errval:  <$rrn>\t$msg\n";

	fieldcolor($row,$rrn,$ERRCOLOR);
}

#---------------------------------------------------------------------------
# Routine:	warnval()
#
# Purpose:	Take a warning action.
#
sub warnval
{
	my $row = shift;				# Errant rollrec's row.
	my $rrn = shift;				# Errant rollrec's name.
	my $msg = shift;				# Warning message.

#	print STDERR "warnval:  <$rrn>\t$msg\n";

	fieldcolor($row,$rrn,$WARNCOLOR);
}

#---------------------------------------------------------------------------
# Routine:	fieldcolor()
#
# Purpose:	Change the text color of a given edit window field.
#
sub fieldcolor
{
	my $row = shift;				# Rollrec's row.
	my $rrn = shift;				# Rollrec's name.
	my $clr = shift;				# New color.

	my $tbl;					# Table to modify.
	my $wij;					# Widget to modify.

	#
	# Get the label widget for the field.
	#
	$tbl = $edittabs{$rrn};
	$wij = $tbl->get($row,$COL_FIELD);

	#
	# Turn the label widget's color.
	#
	$wij->configure(-foreground => $clr);
}

#---------------------------------------------------------------------------
# Routine:	addmsg()
#
# Purpose:	Add a message to one of the edit window's message columns.
#		If the count parameter is zero, then we'll zap the column.
#
sub addmsg
{
	my $rrn = shift;				# Rollrec name.
	my $col = shift;				# Column to modify.
	my $cnt = shift;				# Thing count.
	my $thing = shift;				# Thing.

	my $tbl;					# Table to modify.
	my $lab;					# New label.
	my $str = ' ';					# Error message.

	if($cnt)
	{
		$str = "$cnt $thing";
		$str = $str . 's' if($cnt != 1);
	}

	$tbl = $edittabs{$rrn};
	$lab = $tbl->Label(-text => $str, -anchor => 'w');
	$lab->pack(-fill => 'x', -side => 'top');
	$tbl->put($ROW_MSG,$col,$lab);
}

#---------------------------------------------------------------------------
# Routine:	toggle_ignwarn()
#
# Purpose:	Toggle on /off the "Ignore Edit Warnings" menu command.
#
#		If we're turning off edit warnings, we'll also go through
#		any open edit windows and turn off any "warned" lines.
#		Error lines will remain marked.
#
sub toggle_ignwarn
{
	if($givewarnings)
	{
		$givewarnings = 0;
		$tog_ignwarn->configure(-label => $IGNORE_OFF);

		#
		# Turn off all the warning labels in all the edit windows.
		#
		foreach my $rrn (keys(%edittabs))
		{
			for(my $row = $ROW_FIRST; $row <= $ROW_LAST; $row++)
			{
				my $clr;		# Current row color.
				my $wij;		# Row widget.

				#
				# Skip non-warning rows.
				#
				$wij = $edittabs{$rrn}->get($row,$COL_FIELD);
				$clr = $wij->cget(-foreground);
				next if($clr ne $WARNCOLOR);

				#
				# Change the label color.
				#
				fieldcolor($row,$rrn,$NORMALCOLOR);
			}

			addmsg($rrn,$COL_DATA,0,' ');
		}
	}
	else
	{
		$givewarnings = 1;
		$tog_ignwarn->configure(-label => $IGNORE_ON);
	}
}

#---------------------------------------------------------------------------
# Routine:	toggle_namefilt()
#
# Purpose:	Toggle on /off the "Use Name Filter" menu command.
#
sub toggle_namefilt
{
	if($namefilter)
	{
		$namefilter = 0;
		$tog_namefilt->configure(-label => $FILTER_OFF);
	}
	else
	{
		$namefilter = 1;
		$tog_namefilt->configure(-label => $FILTER_ON);
	}
}

#---------------------------------------------------------------------------
# Routine:	set_btncols()
#
# Purpose:	
#
sub set_btncols
{
	my $err = shift;				# Error flag.

	my $dlg;					# Dialog widget.
	my $lab;					# Label for dialog box.
	my $ent;					# Entry for dialog box.

	my $col;					# New column count.
	my $ret;					# Dialog box return.

	#
	# Create the new dialog box.
	#
	$dlg = $wm->DialogBox(-title	=> 'Set Columns for Button Window',
			      -buttons	=> ["Okay", "Cancel" ]);

	#
	# Add a label to the dialog.
	#
	$lab = $dlg->Label(-text => 'Enter New Column Count:');
	$lab->pack(-side => 'left');

	#
	# Add a text entry slot and focus on the entry.
	#
	$ent = $dlg->add('Entry');
	$ent->pack(-side => 'left');
	$dlg->configure(-focus => $ent);

	#
	# Add a potential error location to the dialog.
	#
	$lab = $dlg->Label(-text => ' ');
	$lab->pack(-side => 'bottom');
	if($err)
	{
		$lab->configure(-text => "Count must be between 1 and $MAXCOLS",
				-foreground => 'red')
	}

	#
	# Show the dialog box and handle cancellations.
	#
	$ret = $dlg->Show();
	return if($ret eq 'Cancel');

	#
	# Get the user's column size.
	#
	$col = $ent->get();

	#
	# If this is an invalid column count, give an error message and
	# ask again.
	#
	set_btncols(1) if(($col < 1) || ($col > $MAXCOLS));

	#
	# Save the new column count and rebuild the table.
	#
	$columncount = $col;
	buildtable(0);
}

#---------------------------------------------------------------------------
# Routine:	editbegone()
#
# Purpose:	Destroy an edit window.  In addition to deleting the window
#		itself, the rollrec is removed from several tables.  These
#		tables are the list of edit windows (%editwinds), and the
#		edit window radio buttons (%editrbs and %editdfs).
#
#		In some cases, this routine will be passed the edit window
#		widget instead of the rollrec name.  If we couldn't find
#		the argument in the edit-window hash, we'll look up the
#		widget itself and recurse with the associated name.
#
sub editbegone
{
	my $rrname = shift;	# Name of rollrec whose editwin we'll destroy.
	my $editwin;		# Edit window to destroy.

# print STDERR "editbegone:  destroying edit window for $rrname\n";

	#
	# Don't do anything for a null name.
	#
	return if($rrname eq '');

	#
	# Deselect the rollrec's button.
	#
	for(my $ind = 0; $ind < $numrrnames; $ind++)
	{
		#
		# Destroy the window and remove it from our edit-window list.
		#
		if($rrnames[$ind] eq $rrname)
		{
			rrname_deselect($ind);
			$editwinds{$rrname}->destroy();
			delete($editwinds{$rrname});
			delete($edittabs{$rrname});

			return;
		}
	}

	#
	# Didn't find the name in the list of rollrec names, so maybe it's
	# a widget itself.  If we find it in the list of edit windows,
	# we'll delete it here.
	#
	foreach my $rrn (keys(%editwinds))
	{
		if($rrn == $rrname)
		{
			$editwinds{$rrname}->destroy();
			delete($editwinds{$rrname});
			delete($edittabs{$rrname});

			return;
		}
	}
}

#---------------------------------------------------------------------------
# Routine:	renamebegone()
#
# Purpose:	Destroy a rename window.  In addition to deleting the window
#		itself, the rollrec is removed from several tables.  These
#		tables are the list of rename windows (%renwinds).
#
#		In some cases, this routine will be passed the rename window
#		widget instead of the rollrec name.  If we couldn't find
#		the argument in the rename-window hash, we'll look up the
#		widget itself and recurse with the associated name.
#
sub renamebegone
{
	my $oldname = shift;				# Old name of rollrec.
	my $newname = shift;				# New name of rollrec.

# print STDERR "renamebegone:  destroying rename window for $oldname\n";

	#
	# Don't do anything for a null name.
	#
	return if($oldname eq '');
	return if($newname eq '');

	#
	# Destroy the old name's rename window and remove it from our
	# rename-window list.
	#
	if(exists($renwinds{$oldname}))
	{
		$renwinds{$oldname}->destroy();
		delete($renwinds{$oldname});
		delete($rentabs{$oldname});
	}

	#
	# Remove the new name from our rename-window list.
	#
	delete($renwinds{$newname});
	delete($rentabs{$newname});
}

#---------------------------------------------------------------------------
# Routine:	helpbegone()
#
# Purpose:	Destroy a help window.
#
sub helpbegone
{
	$helpwin->destroy();
	$inhelpwind = 0;
}

#############################################################################

#---------------------------------------------------------------------------
# Routine:	errorbox()
#
# Purpose:	Display an error dialog box.
#
sub errorbox
{
	my $msg  = shift;			# Warning message.
	my $dlg;				# Warning dialog widget.

	$dlg = $wm->Dialog(-title => "$NAME Error",
			   -text  => $msg,
			   -default_button => "Okay",
			   -buttons => ["Okay"]);
	$dlg->Show();
}

#---------------------------------------------------------------------------
# Routine:	errorbox_multi()
#
# Purpose:	Display a multiline error dialog box.  Newlines in the message
#		signal a new line (implemented with a new label) in the dialog
#		box.
#
sub errorbox_multi
{
	my $msgs  = shift;			# Messages to display.

	my $dlg;				# Warning dialog widget.
	my $lab;				# Label for table.

	my @lines;				# Lines in message.
	my $line;				# Individual message line.

	$dlg = $wm->DialogBox(-title	=> "$NAME Error",
			      -buttons	=> ["Okay"]);

	@lines = split /\n/, $msgs;

	foreach $line (@lines)
	{
		$lab = $dlg->Label(-text => $line);
		$lab->pack(-side => 'top');
	}

	$dlg->Show();
}

#---------------------------------------------------------------------------
# Routine:	dbx_dups()
#
# Purpose:	Puts up a dialog box with names of the duplicated rollrecs.
#		This is used for the "Merge Rollrecs" command.
#
sub dbx_dups
{
	my @dupnames = @_;			# Duplicated rollrecs.

	my $dbx;				# Dialog box.
	my $rrtab;				# Dup'd rollrec table.
	my $lab;				# Label for table.
	my $rowind = 1;				# Table row index.

	my $rows = @dupnames + 1;		# Rows in table.
	my $sbars  = 'e';			# Scrollbars.
	my $ret;				# Dialog return.

	#
	# Build a dialog box to hold an error summary.
	#
	$dbx = $wm->DialogBox(-title   => 'Duplicated Rollrecs',
			      -buttons => ["Merge", "Cancel" ]);

	#
	# If there's only a few rows, we'll dump the scrollbar.
	#
	$sbars	= '' if($rows < 4);

	#
	# Build a table to hold the duplicated rollrec names.
	#
	$rrtab = $dbx->Table(-rows		=> $rows,
			     -columns		=> 1,
			     -relief		=> 'raised',
			     -scrollbars	=> $sbars,
			     -borderwidth	=> 1,
			     -fixedrows		=> 0,
			     -takefocus		=> 1,
			    );

	#
	# Add a column header.
	#
	$lab = $rrtab->Label(-text => 'Duplicated Rollrecs');
	$rrtab->put(0,0,$lab);

	#
	# Add the selected rollrec names to the table.
	#
	foreach my $rrn (sort {$a <=> $b} @dupnames)
	{
		$lab = $rrtab->Label(-text => $rrn, -anchor => 'w');
		$rrtab->put($rowind,0,$lab);
		$rowind++;
	}

	#
	# Pack the error table and display the dialog box.
	#
	$rrtab->pack(-side => 'top');
	$ret = $dbx->Show();

	return($ret);
}

#---------------------------------------------------------------------------
# Routine:	help_help()
#
# Purpose:	Display a help window.
#
sub help_help
{
	my $hframe;					# Help frame.
	my $wdgt;					# General widget.

	my $helpstr;

	$helpstr = "

rollrec-editor - DNSSEC-Tools Rollrec GUI Editor
         
SYNOPSIS
         
    rollrec-editor [options] <rollrec-file>

DESCRIPTION

rollrec-editor provides the capability for easy GUI-based management of
rollrec files.  A rollrec file contains one or more rollrec records.  These
records are used by the DNSSEC-Tools rollover utilities (rollerd, etc.) to
describe zones' rollover state.  Each zone's rollrec record contains such
information as the zone file, the rollover phase, and logging level.  rollrec
files are text files and may be edited by any text editor.  rollrec-editor
allows editing of only those records a user should change and performs error
checking on the data.

When rollrec-editor starts, a window is created that has \"buttons\" for each
rollrec record in the given rollrec file.  (In this documentation, this window
is called the Button Window.)  Clicking on the buttons selects (or deselects)
that zone.  After one or more zones are selected, one of several commands may
be executed.  Commands allow modification and deletion of existing rollrec
records, creation of new rollrec records, merging of rollrec files, and
verification of file validity.

rollrec-editor's commands are available through the menus and most have a
keyboard accelerator.  The commands are described in the manual page.

When a rollrec record is selected for modification, a new window is created to
hold the editable fields of the record.  The fields may be modified in place.
When editing is complete, the record is \"saved\".  This does not save the
modified rollrec into its on-disk file; the file must be saved explicitly from
the Button Window.

As stated above, verification checks are performed when saving an edited
rollrec record.  One set of checks ensures that files and directories
associated with a zone actually exist.  This check may be turned off at
command start-up with the -ignore-warns command line option.  It may be
modified during execution with the \"Ignore Edit Warnings\" menu command.

More information may be found in rollrec-editor's man page.

";

	#
	# If we've already got another help window, we'll give an error and
	# return.  Otherwise, we'll turn on our in-helpwindow flag.
	#
	if($inhelpwind)
	{
		errorbox("Multiple help windows cannot be created\n");
		return;
	}
	$inhelpwind = 1;

	#
	# Create a new window to hold our help info.  Bind up some
	# key accelerators, too.
	#
	$helpwin = MainWindow->new(-relief => 'raised',
				  -title  => 'Help!',
				  -borderwidth => 1);
	$helpwin->bind('<Control-Key-q>',\&file_quit);
	$helpwin->bind('<Control-Key-w>',\&helpbegone);

	#
	# Now make the containers for the window.
	#
	$hframe = $helpwin->Frame(-relief => 'raised', -borderwidth => 1);

	$hframe->pack(-fill => 'x');

	#
	# Add the help data to the frame.
	#
	$wdgt = $hframe->Label(-text => $helpstr,
			       -justify => 'left');
	$wdgt->pack(-side => 'top');

	#
	# Add a button to dismiss the window.
	#
	$wdgt = $hframe->Button(-text => 'Done',
				-command => \&helpbegone);
	$wdgt->pack(-side => 'top');
}

#----------------------------------------------------------------------
#
# Routine:      settitle()
#
# Purpose:      Set the title for use in the "Editing File" line.
#
sub settitle
{
	my $name = shift;				# Name to use.

	if($modified)
	{
		$name .= "    (modified)" if(!$modset);
		$modset = 1;
	}
	$title = $name;
}

#----------------------------------------------------------------------
#
# Routine:      version()
#
# Purpose:      Print the version number(s) and exit.
#
sub version
{
	print STDERR "$VERS\n";
	print STDERR "$DTVERS\n";
	exit(0);
}

#---------------------------------------------------------------------------
# Routine:	usage()
#
# Purpose:      Print a usage message and exit.
#
sub usage
{
	print STDERR "usage:  rollrec-editor [options] <rollrec-file>\n";
	print STDERR "\toptions:\n";
	print STDERR "\t\t-ignore-warns		ignore edit warnings\n";
	print STDERR "\t\t-no-filter		turn off name filtering\n";
	print STDERR "\t\t-columns columns	columns in button window\n";
	print STDERR "\t\t-V			program version\n";
	exit(0);
}

1;

#############################################################################

=pod

=head1 NAME

rollrec-editor - DNSSEC-Tools Rollrec GUI Editor

=head1 SYNOPSIS

  rollrec-editor [options] <rollrec-file>

=head1 DESCRIPTION

B<rollrec-editor> provides the capability for easy GUI-based management of
I<rollrec> files.  A B<rollrec> file contains one or more I<rollrec> records.
These records are used by the DNSSEC-Tools rollover utilities (B<rollerd>,
etc.) to describe zones' rollover state.  Each zone's I<rollrec> record
contains such information as the zone file, the rollover phase, and logging
level.  I<rollrec> files are text files and may be edited by any text editor.
B<rollrec-editor> allows editing of only those records a user should change
and performs error checking on the data.

When B<rollrec-editor> starts, a window is created that has "buttons" for each
I<rollrec> record in the given I<rollrec> file.  (In this documentation, this
window is called the Button Window.)  Clicking on the buttons selects (or
deselects) that zone.  After one or more zones are selected, one of several
commands may be executed.  Commands allow modification and deletion of
existing I<rollrec> records, creation of new I<rollrec> records, merging of
I<rollrec> files, and verification of file validity.

B<rollrec-editor>'s commands are available through the menus and most have a
keyboard accelerator.  The commands are described below in the COMMANDS
section.

When a I<rollrec> record is selected for modification, a new window is created
to hold the editable fields of the record.  The fields may be modified in
place.  When editing is complete, the record is "saved".  This does not save
the modified I<rollrec> into its on-disk file; the file must be saved
explicitly from the Button Window.

As stated above, verification checks are performed when saving an edited
I<rollrec> record.  One set of checks ensures that files and directories
associated with a zone actually exist.  This check may be turned off at
command start-up with the B<-ignore-warns> command line option.  It may be
modified during execution with the "Ignore Edit Warnings" menu command.

=head2 Button Window Layout

The Button Window contains a button for each I<rollrec> record in the
selected file.  The buttons are arranged in a table that with at least three
columns.  The number of columns may be set at command start-up with the 
B<-columns> command line option.  It may be modified during execution with
the "Columns in Button Window" menu command.

When setting the number of columns, B<rollrec-editor> minimizes the space
required to display the selected file's I<rollrec> buttons.  This will
sometimes cause the number of columns to differ from that requested.

For example, assume a I<rollrec> file has 12 I<rollrec> records.  The
following table shows how many rows and columns are given for each of
the given column selections.

	column count	rows	columns
	      1		 12	   1
	      2		 6	   2
	      3		 4	   3
	      4		 3	   4
	      5		 3	   4
	      6		 2	   6
	      7		 2	   6
	      8		 2	   6
	      9		 2	   6
	      10	 2	   6
	      11	 2	   6
	      12	 1	   12

The actual rows and columns for a requested column count will vary in order
to allow a "best-fit".

=head2 UNDOING MODIFICATIONS

B<The "undo" capability is not currently implemented.>

B<rollrec-editor> has the ability to reverse modifications it has made to a
I<rollrec> file.  This historical restoration will only work for modifications
made during a particular execution of B<rollrec-editor>; modifications made
during a previous execution may not be undone.

After a "Save" operation, the data required for reversing modifications are
deleted.  This is not the case for the "Save As" operation.

=head1 OPTIONS

B<rollrec-editor> supports the following options.

=over 4

=item B<-columns columns>

This option allows the user to specify the number of columns to be used in
the Button Window.

=item B<-ignore-warns>

This option causes B<rollrec-editor> to ignore edit warnings when performing
validation checks on the contents of I<rollrec> records.

=item B<-no-filter>

This option turns off name filtering when B<rollrec-editor> presents a
file-selection dialog for choosing a new I<rollrec> file.  If this option
is not given, then the file-selection dialog will only list regular files
with a suffix of B<.rrf>.

=item B<-Version>

Displays the version information for B<rollrec-editor> and the DNSSEC-Tools
package.

=back

=head1 COMMANDS

B<rollrec-editor> provides the following commands, organized by menus:
File, Edit, Commands, Rollrecs, and Options.

=head2 File Menu

The File Menu contains commands for manipulating I<rollrec> files and
stopping execution.

=over 4

=item B<Open>

Open a new I<rollrec> file.  If the specified file does not exist, the user
will be prompted for the action to take.  If the user chooses the "Continue"
action, then B<rollrec-editor> will continue editing the current I<rollrec>
file.  If the "Quit" action is selected, then B<rollrec-editor> will exit.

=item B<Save>

Save the current I<rollrec> file.  The data for the "Undo Changes" command are
purged, so this file will appear to be unmodified.

Nothing will happen if no changes have been made.

=item B<Save As>

Save the current I<rollrec> file to a name selected by the user.

=item B<Quit>

Exit B<rollrec-editor>.

=back

=head2 Edit Menu

The Edit Menu contains commands for general editing operations.

=over 4

=item B<Undo Changes>

Reverse modifications made to the I<rollrec> records.  This is B<only>
for the in-memory version of the I<rollrec> file.

B<Not currently implemented.>

=back

=head2 Commands Menu

The Commands Menu contains commands for modifying I<rollrec> records.

=over 4

=item B<New Rollrec>

Create a new I<rollrec> record.   The user is given a new window in which to
edit the user-modifiable I<rollrec> fields.  A button for the new I<rollrec>
record will be inserted into the Button Window.

After editing is completed, the "Save" button will add the new I<rollrec>
record to the in-memory I<rollrec> file.  The file must be saved in order to
have the new I<rollrec> added to the file.

Potentially erroneous conditions will be reported to the user at save time.  
If the I<ignore-warnings> flag has been turned on, then these warnings will
not be reported.  Errors (e.g., invalid log conditions) will always be
reported.

=item B<Delete Selected Rollrecs>

Delete the selected I<rollrec> records.  The buttons for each selected record
will be removed from the Button Window.

=item B<Edit Selected Rollrecs>

Modify the selected I<rollrec> records.   For every record selected
for modification, the user is given a new window in which to edit the
user-modifiable I<rollrec> fields.  When the edit window goes away (after a
"Save" or "Cancel"), the I<rollrec> record's button is deselected.

After editing is completed, the "Save" button will add the new I<rollrec>
record to the in-memory I<rollrec> file.  The file must be saved in order to
have the new I<rollrec> added to the file.

Potentially erroneous conditions will be reported to the user at save time.  
If the I<ignore-warnings> flag has been turned on, then these warnings will
not be reported.  Errors (e.g., invalid log conditions) will always be
reported.

=item B<Rename Selected Rollrec>

Rename the selected I<rollrec> record.   The name in the I<rollrec>'s "roll"
or "skip" line will be changed to the new name.  Only one I<rollrec> may be
renamed at a time, and it may not be edited while the rename is taking place.

If the I<rollrec> has been modified, the new contents B<must> be written to
disk prior to the rename happening.  The user will be prompted to ensure that
this the user wishes to continue.

=item B<Merge Rollrec Files>

Merge a I<rollrec> file with the currently open I<rollrec> file.  After a
successful merge, the I<rollrec> records in the second file will be added 
to the I<end> of the currently open I<rollrec> file.

If there are any I<rollrec> name collisions in the files, then the user will
be asked whether to continue with the merge or cancel it.  If the merge
continues, then the conflicting I<rollrec> records from the "new" file will be
discarded in favor of the currently open I<rollrec> file.

=item B<Verify Rollrec File>

Verify the validity of the I<rollrec> file.  The user-editable fields in the
open I<rollrec> file are checked for validity.  An edit window is opened for
each I<rollrec> record that registers an error or warning.

If the I<ignore-warnings> flag has been turned on, then potentially erroneous
conditions will not be reported.  Errors (e.g., invalid log conditions) will
always be reported.

=item B<Summarize Problems>

Summarize the warning and error counts of the I<rollrec> file.  Each
I<rollrec> record in the open I<rollrec> file is checked for validity.  If
any warnings or errors are found, a window is displayed that lists the name
of each I<rollrec> record and its warning and error counts.

If the I<ignore-warnings> flag has been turned on, then potentially erroneous
conditions will not be reported.  Errors (e.g., invalid log conditions) will
always be reported.

=back

=head2 View Menu

The View Menu contains miscellaneous commands for viewing edit windows.

=over 4

=item B<Select All Rollrecs/Unselect All Rollrecs>

Select or unselect all I<rollrec> buttons.  All the buttons in the Button
Window will be selected or unselected.  If B<any> of the buttons are not
selected, this command will cause all the buttons to become selected.  If
I<all> of the buttons are selected, this command will cause all the buttons
to be deselected.

This command is a toggle that switches between Select All Rollrecs and
Unselect All Rollrecs.

=item B<Reveal Rollrec Edit Windows>

Raise all I<rollrec> edit windows.  This command brings all I<rollrec> edit
windows to the front so that any that are hidden behind other windows will
become visible.

=item B<Close Rollrec Edit Windows>

Close all I<rollrec> edit windows.  This command closes and deselects all
I<rollrec> edit windows.

=back

=head2 Options Menu

The Options Menu allows the user to set several options that control
B<rollrec-editor> behavior.

=over 4

=item B<Ignore Edit Warnings/Don't Ignore Edit Warnings>

Certain operations perform validation checks on the contents of I<rollrec>
records.  Warnings and errors may be reported by these checks.  This option 
controls whether or not warnings are flagged during validation.  If they
are flagged, then the operation will not continue until the warning condition
is fixed or the operation canceled.  If the warnings are ignored, then the
operation will complete without the condition being fixed.

This command is a toggle that switches between Ignore Edit Warnings mode and
Don't Ignore Edit Warnings mode.

=item B<Filter Name Selection/Don't Filter Name Selection>

When opening I<rollrec> files for editing or merging, a file-selection dialog
box is displayed to allow the user to select a I<rollrec> file.  This option
controls whether a filename filter is used by the dialog box.  If Filter Names
Selection mode is used, then only regular files with a suffix of B<.rrf> will
be displayed by the dialog box.  If Don't Filter Name Selection mode is used,
then all regular files will be displayed by the dialog box.

This command is a toggle that switches between Filter Name Selection display
and Don't Filter Name Selection display.

=item B<Columns in Button Window>

Set the number of columns used in the Button Window.  See the Button Window
Layout section for more information on columns in the Button Window.

=back

=head2 Help Menu

The Help Menu contains commands for getting assistance.

=over 4

=item B<Help>

Display a help window.

=back

=head1 KEYBOARD ACCELERATORS

Below are the keyboard accelerators for the B<rollrec-editor> commands:

    Ctrl-A  Select All Rollrecs

    Ctrl-D  Delete Selected Rollrecs

    Ctrl-E  Edit Selected Rollrecs

    Ctrl-H  Help

    Ctrl-K  Close Rollrec Edit Windows

    Ctrl-M  Merge Rollrec Files

    Ctrl-N  New Rollrec

    Ctrl-O  Open

    Ctrl-Q  Quit

    Ctrl-R  Reveal Rollrec Edit Windows

    Ctrl-S  Save

    Ctrl-U  Undo Changes  (not currently implemented)

    Ctrl-V  Verify Rollrec File

These accelerators are all lowercase letters.

=head1 REQUIREMENTS

B<rollrec-editor> is implemented in Perl/Tk, so both Perl and Perl/Tk must be
installed on your system.

=head1 KNOWN ISSUES

The following are known issues.  These will be resolved in the fullness of time.

=over 4

=item

There is an issue with the column count and adding new I<rollrecs>.
It doesn't always work properly, thus occasionally leaving some I<rollrec>
buttons undisplayed.

=item

There is no undo command.

=back

=head1 COPYRIGHT

Copyright 2007-2014 SPARTA, Inc.  All rights reserved.
See the COPYING file included with the DNSSEC-Tools package for details.

=head1 AUTHOR

Wayne Morrison, tewok@tislabs.com

=head1 SEE ALSO

B<lsroll(1)>,
B<rollchk(8)>,
B<rollerd(8)>
B<rollinit(8)>,
B<rollset(8)>,
B<zonesigner(8)>

B<Net::DNS::SEC::Tools::rollrec(3)>

B<file-rollrec(5)>

=cut
