/*
 *   xmcd - Motif(tm) CD Audio Player
 *
 *   Copyright (C) 1993-2001  Ti Kan
 *   E-mail: xmcd@amb.org
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */
#ifndef LINT
static char *_dbprog_c_ident_ = "@(#)dbprog.c	7.163 01/07/02";
#endif

#include "common_d/appenv.h"
#include "common_d/patchlevel.h"
#include "common_d/util.h"
#include "xmcd_d/xmcd.h"
#include "xmcd_d/widget.h"
#include "cdinfo_d/cdinfo.h"
#include "xmcd_d/callback.h"
#include "xmcd_d/cdfunc.h"
#include "xmcd_d/wwwwarp.h"
#include "xmcd_d/userreg.h"
#include "xmcd_d/dbprog.h"
#include "libdi_d/libdi.h"


extern widgets_t	widgets;
extern appdata_t	app_data;
extern FILE		*errfp;

STATIC int		time_mode,		/* Time display mode flag */
			sel_pos = -1,		/* Track list select position */
			ind_pos = -1,		/* Track list highlight pos */
			extt_pos = -1,		/* Selected track position */
			cred_pos = -1,		/* Cred list select position */
			seg_pos = -1,		/* Seg list select position */
			dlist_pos = -1,		/* Disc list select position */
			dlist_mode,		/* Disc list mode */
			fname_mode,		/* Full name window mode */
			reg_mode,		/* Region selector mode */
			credits_mode,		/* Credits window mode */
			hist_cnt = 0,		/* Hist list count */
			reg_cnt = 0,		/* Region list count */
			match_cnt = 0,		/* Matchsel list count */
			start_slot = -1,	/* Changer scan start slot */
			scan_slot = -1;		/* Changer scan current slot */
STATIC long		scan_id = -1;		/* Changer scan timeout ID */
STATIC bool_t		title_edited = FALSE,	/* Track title edited flag */
			extt_setup = FALSE,	/* Track details setup */
			auth_initted = FALSE,	/* User entered auth info */
			hist_initted = FALSE,	/* History initialized */
			auto_trk = FALSE,	/* Auto-track */
			sav_mplay = FALSE,	/* Original multiplay mode */
			sav_rev = FALSE,	/* Original reverse mode */
			wwwwarp_cleared = FALSE,/* wwwWarp menu cleared */
			stopload_active = FALSE,/* Stop load dialog active */
			fname_changed = FALSE;	/* Full name text changed */
STATIC XmString		xs_extd_lbl,		/* Disc details lbl */
			xs_extd_lblx,		/* Disc details lbl (*) */
			xs_dcred_lbl,		/* Disc cred lbl */
			xs_dcred_lblx,		/* Disc cred lbl (*) */
			xs_seg_lbl,		/* Segments lbl */
			xs_seg_lblx,		/* Segments lbl (*) */
			xs_extt_lbl,		/* Track details lbl */
			xs_extt_lblx,		/* Track details lbl (*) */
			xs_tcred_lbl,		/* Track credits lbl */
			xs_tcred_lblx,		/* Track credits lbl (*) */
			xs_scred_lbl,		/* Segment credits lbl */
			xs_scred_lblx;		/* Segment credits lbl (*) */
STATIC cdinfo_incore_t	*dbp;			/* Pointer to CD info struct */
STATIC cdinfo_credit_t	w_cred;			/* Work credit struct */
STATIC cdinfo_segment_t	w_seg;			/* Work segment struct */

/* Function prototypes */
STATIC void		dbprog_hist_new(curstat_t *),
			dbprog_chgr_new(curstat_t *);


/***********************
 *  internal routines  *
 ***********************/


/*
 * dbprog_fullname_ck
 *	Check the fullname structure fields for correctness.  Pop up a
 *	informational message dialog box if it does not pass.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	TRUE  - passed
 *	FALSE - failed
 */
STATIC bool_t
dbprog_fullname_ck(curstat_t *s)
{
	cdinfo_fname_t	*fnp;
	char		*msg,
			buf[STR_BUF_SZ],
			buf2[STR_BUF_SZ];

	switch (fname_mode) {
	case FNAME_DISC:
		fnp = &dbp->disc.artistfname;
		(void) sprintf(buf, "%.31s", app_data.str_albumartist);
		break;

	case FNAME_TRACK:
		if (extt_pos < 0)
			return TRUE;	/* Nothing to check */

		fnp = &dbp->track[extt_pos].artistfname;
		(void) sprintf(buf2, "%.31s", app_data.str_trackartist);
		(void) sprintf(buf, buf2, s->trkinfo[extt_pos].trkno);
		break;

	case FNAME_CREDITS:
		fnp = &w_cred.crinfo.fullname;
		(void) sprintf(buf, "%.31s", app_data.str_credit);
		break;

	default:
		return TRUE;		/* Nothing to check */
	}
	(void) strcat(buf, " full name:\n");

	/* Check fullname for correctness */
	if (fnp->firstname == NULL && fnp->lastname != NULL) {
		msg = (char *) MEM_ALLOC(
			"fullname_ck_msg",
			strlen(buf) + strlen(app_data.str_nofirst) + 1
		);
		if (msg == NULL) {
			CD_FATAL(app_data.str_nomemory);
			return FALSE;
		}
		(void) strcpy(msg, buf);
		(void) strcat(msg, app_data.str_nofirst);
		CD_INFO(msg);
		MEM_FREE(msg);

		/* Put the focus on the first name field */
		XmProcessTraversal(
			widgets.fullname.firstname_txt,
			XmTRAVERSE_CURRENT
		);
		return FALSE;
	}
	if (fnp->the != NULL && fnp->firstname == NULL &&
	    fnp->lastname == NULL) {
		msg = (char *) MEM_ALLOC(
			"fullname_ck_msg",
			strlen(buf) + strlen(app_data.str_nofirstlast) + 1
		);
		if (msg == NULL) {
			CD_FATAL(app_data.str_nomemory);
			return FALSE;
		}
		(void) strcpy(msg, buf);
		(void) strcat(msg, app_data.str_nofirstlast);
		CD_INFO(msg);
		MEM_FREE(msg);

		/* Put the focus on the first name field */
		XmProcessTraversal(
			widgets.fullname.firstname_txt,
			XmTRAVERSE_CURRENT
		);
		return FALSE;
	}

	return TRUE;
}


/*
 * dbprog_dpytottime
 *	Display the disc total time in the total time indicator
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_dpytottime(curstat_t *s)
{
	XmString	xs;
	char		str[STR_BUF_SZ];
	static char	prev[STR_BUF_SZ];

	if (s->mode == MOD_BUSY || s->mode == MOD_NODISC)
		(void) strcpy(str, "Total: --:--");
	else
		(void) sprintf(str, "Total: %02d:%02d",
				s->discpos_tot.min, s->discpos_tot.sec);

	if (strcmp(str, prev) == 0)
		return;

	xs = XmStringCreateLtoR(str, XmSTRING_DEFAULT_CHARSET);

	XtVaSetValues(
		widgets.dbprog.tottime_ind,
		XmNlabelString,
		xs,
		NULL
	);

	XmStringFree(xs);

	(void) strcpy(prev, str);
}


/*
 * dbprog_list_autoscroll
 *	Scroll a list if necessary to make the specified item visible.
 *
 * Args:
 *	list - The list widget to operate on
 *	nitems - The total number of items in the list
 *	pos - The list position to make visible
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_list_autoscroll(Widget list, int nitems, int pos)
{
	int	n,
		top_pos,
		bottom_pos,
		vis_cnt;

	if (nitems == 0)
		return;	/* Nothing to do */

	/* Determine range of items visible in current list */
	XtVaGetValues(
		list,
		XmNtopItemPosition, &top_pos,
		XmNvisibleItemCount, &vis_cnt,
		NULL
	);

	bottom_pos = top_pos + vis_cnt - 1;

	/* Try to keep the desired items near the middle of the visible
	 * portion of the list.
	 */
	if (pos < top_pos) {
		/* Scroll up */
		n = pos - (vis_cnt / 2);
		if (n < 1)
			n = 1;
		XmListSetPos(list, n);
	}
	else if (pos > bottom_pos) {
		/* Scroll down */
		n = pos + (vis_cnt / 2);
		if (n > nitems)
			n = nitems;
		XmListSetBottomPos(list, n);
	}
}


/*
 * dbprog_listupd_ent
 *	Update a track entry in the track list.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	pos - Track position to update.
 *	title - Track title string.
 *	newent - Whether this is a new entry or a replacement entry.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_listupd_ent(curstat_t *s, int pos, char *title, bool_t newent)
{
	int		n,
			secs,
			min,
			sec;
	char		*str;
	XmString	xs;

	if (s->trkinfo[pos].trkno < 0)
		return;

	if (time_mode == TIME_TOTAL) {
		min = s->trkinfo[pos].min;
		sec = s->trkinfo[pos].sec;
	}
	else {
		secs = ((s->trkinfo[pos+1].min * 60 + s->trkinfo[pos+1].sec) - 
			(s->trkinfo[pos].min * 60 + s->trkinfo[pos].sec));
		min = (byte_t) (secs / 60);
		sec = (byte_t) (secs % 60);
	}

	n = strlen(title) + strlen(TRKLIST_FMT) + 1;
	if ((str = (char *) MEM_ALLOC("listupd_ent_str", n)) == NULL) {
		CD_FATAL(app_data.str_nomemory);
		return;
	}

	(void) sprintf(str, TRKLIST_FMT,
		       s->trkinfo[pos].trkno, min, sec, title,
		       (dbp->track[pos].notes != NULL ||
			dbp->track[pos].credit_list != NULL) ?
				ASTERISK_STR : "");

	if (s->mode != MOD_BUSY && s->mode != MOD_NODISC &&
	    s->cur_trk >= 0 && di_curtrk_pos(s) == pos)
		xs = XmStringCreate(str, CHSET2);
	else
		xs = XmStringCreate(str, CHSET1);

	if (newent)
		XmListAddItemUnselected(widgets.dbprog.trk_list, xs, pos+1);
	else
		XmListReplaceItemsPos(widgets.dbprog.trk_list, &xs, 1, pos+1);

	XmStringFree(xs);
	MEM_FREE(str);
}


/*
 * dbprog_listupd_all
 *	Update the track list display to reflect the contents of
 *	the trkinfo table in the curstat_t structure.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	newent - Whether this is a new entry or a replacement entry.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_listupd_all(curstat_t *s, bool_t newent)
{
	int	i;

	if (newent)
		XmListDeleteAllItems(widgets.dbprog.trk_list);

	for (i = 0; i < (int) s->tot_trks; i++) {
		/* Update track list entry */
		dbprog_listupd_ent(
			s, i,
			(dbp->track[i].title != NULL) ?
				dbp->track[i].title : UNDEF_STR,
			newent
		);
	}

	/* If this item is previously selected, re-select it */
	if (sel_pos > 0)
		XmListSelectPos(widgets.dbprog.trk_list, sel_pos, False);
	else if (ind_pos > 0)
		XmListSelectPos(widgets.dbprog.trk_list, ind_pos, False);
}


/*
 * dbprog_extd_lblupd
 *	Update the disc details button label to indicate whether there is
 *	text contained in the disc notes.
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_extd_lblupd(void)
{
	XmString	xs;
	bool_t		newstate;
	static bool_t	state = FALSE;

	newstate = (bool_t) (dbp->disc.notes != NULL);
	if (newstate) {
		if (state)
			return;		/* no change */
		xs = xs_extd_lblx;
	}
	else {
		if (!state)
			return;		/* no change */
		xs = xs_extd_lbl;
	}

	XtVaSetValues(widgets.dbprog.extd_btn,
		XmNlabelString, xs,
		NULL
	);

	state = newstate;
}


/*
 * dbprog_dcred_lblupd
 *	Update the disc credits button label to indicate whether there are
 *	disc credits defined.
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_dcred_lblupd(void)
{
	XmString	xs;
	bool_t		newstate;
	static bool_t	state = FALSE;

	newstate = (bool_t) (dbp->disc.credit_list != NULL);
	if (newstate) {
		if (state)
			return;		/* no change */
		xs = xs_dcred_lblx;
	}
	else {
		if (!state)
			return;		/* no change */
		xs = xs_dcred_lbl;
	}

	XtVaSetValues(widgets.dbprog.dcredits_btn,
		XmNlabelString, xs,
		NULL
	);

	state = newstate;
}


/*
 * dbprog_seg_lblupd
 *	Update the segments button label to indicate whether there are
 *	segments defined.
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_seg_lblupd(void)
{
	XmString	xs;
	bool_t		newstate;
	static bool_t	state = FALSE;

	newstate = (bool_t) (dbp->disc.segment_list != NULL);
	if (newstate) {
		if (state)
			return;		/* no change */
		xs = xs_seg_lblx;
	}
	else {
		if (!state)
			return;		/* no change */
		xs = xs_seg_lbl;
	}

	XtVaSetValues(widgets.dbprog.segments_btn,
		XmNlabelString, xs,
		NULL
	);

	state = newstate;
}


/*
 * dbprog_extt_lblupd
 *	Update the track details button label to indicate whether there is
 *	text contained in the track notes or track credits section.
 *
 * Args:
 *	t - Pointer to the cdinfo_track_t structure pertaining to the
 *	    currently selected track.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_extt_lblupd(cdinfo_track_t *t)
{
	XmString	xs;
	bool_t		newstate;
	static bool_t	state = FALSE;

	newstate = (bool_t) (t != NULL && t->notes != NULL);
	if (newstate) {
		if (state)
			return;		/* no change */
		xs = xs_extt_lblx;
	}
	else {
		if (!state)
			return;		/* no change */
		xs = xs_extt_lbl;
	}

	XtVaSetValues(widgets.dbprog.extt_btn,
		XmNlabelString, xs,
		NULL
	);

	state = newstate;
}


/*
 * dbprog_tcred_lblupd
 *	Update the track credits button label to indicate whether there are
 *	track credits defined for the designated track.
 *
 * Args:
 *	t - Pointer to the cdinfo_track_t structure pertaining to the
 *	    currently selected track.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_tcred_lblupd(cdinfo_track_t *t)
{
	XmString	xs;
	bool_t		newstate;
	static bool_t	state = FALSE;

	newstate = (bool_t) (t != NULL && t->credit_list != NULL);
	if (newstate) {
		if (state)
			return;		/* no change */
		xs = xs_tcred_lblx;
	}
	else {
		if (!state)
			return;		/* no change */
		xs = xs_tcred_lbl;
	}

	XtVaSetValues(widgets.dbprog.tcredits_btn,
		XmNlabelString, xs,
		NULL
	);

	state = newstate;
}


/*
 * dbprog_scred_lblupd
 *	Update the segment credits button label to indicate whether there are
 *	segment credits defined.
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_scred_lblupd(void)
{
	XmString	xs;
	bool_t		newstate;
	static bool_t	state = FALSE;

	newstate = (bool_t) (w_seg.credit_list != NULL);
	if (newstate) {
		if (state)
			return;		/* no change */
		xs = xs_dcred_lblx;
	}
	else {
		if (!state)
			return;		/* no change */
		xs = xs_dcred_lbl;
	}

	XtVaSetValues(widgets.segments.credits_btn,
		XmNlabelString, xs,
		NULL
	);

	state = newstate;
}


/*
 * dbprog_set_disc_title
 *	Set the disc title label on the disc details and credits windows
 *
 * Args:
 *	discno - The disc number
 *	artist_str - The artist string
 *	title_str - The title string
 *
 * Return:
 *	Nothing;
 */
STATIC void
dbprog_set_disc_title(int discno, char *artist, char *title)
{
	XmString	xs;
	char		buf[16],
			dtitle[TITLEIND_LEN];

	(void) sprintf(buf, "Disc %d", discno);
	(void) sprintf(dtitle, "%.128s%s%.128s",
			(artist == NULL) ? "" : artist,
			(artist != NULL && title != NULL) ? " / " : "",
			(title == NULL) ? app_data.str_unkndisc : title);

	xs = XmStringCreateSimple(buf);
	XtVaSetValues(widgets.dbextd.discno_lbl,
		XmNlabelString, xs,
		NULL
	);
	XtVaSetValues(widgets.segments.discno_lbl,
		XmNlabelString, xs,
		NULL
	);
	if (credits_mode == CREDITS_DISC) {
		XtVaSetValues(widgets.credits.disctrk_lbl,
			XmNlabelString, xs,
			NULL
		);
	}
	XmStringFree(xs);

	xs = XmStringCreateLtoR(dtitle, XmSTRING_DEFAULT_CHARSET);
	XtVaSetValues(widgets.dbextd.disc_lbl,
		XmNlabelString, xs,
		NULL
	);
	XtVaSetValues(widgets.segments.disc_lbl,
		XmNlabelString, xs,
		NULL
	);
	if (credits_mode == CREDITS_DISC) {
		XtVaSetValues(widgets.credits.title_lbl,
			XmNlabelString, xs,
			NULL
		);
	}
	XmStringFree(xs);
}


/*
 * dbprog_set_track_title
 *	Set the track title label on the track details window
 *
 * Args:
 *	trkno - The track number
 *	str - The label string
 *
 * Return:
 *	Nothing;
 */
STATIC void
dbprog_set_track_title(int trkno, char *str)
{
	XmString	xs;
	char		buf[16];

	(void) sprintf(buf, "Track %d", trkno);

	xs = XmStringCreateSimple(buf);
	XtVaSetValues(widgets.dbextt.trkno_lbl,
		XmNlabelString, xs,
		NULL
	);
	if (credits_mode == CREDITS_TRACK) {
		XtVaSetValues(widgets.credits.disctrk_lbl,
			XmNlabelString, xs,
			NULL
		);
	}
	XmStringFree(xs);

	xs = XmStringCreateLtoR(
		str == NULL ? "<Untitled>" : str,
		XmSTRING_DEFAULT_CHARSET
	);
	XtVaSetValues(widgets.dbextt.trk_lbl,
		XmNlabelString, xs,
		NULL
	);
	if (credits_mode == CREDITS_TRACK) {
		XtVaSetValues(widgets.credits.title_lbl,
			XmNlabelString, xs,
			NULL
		);
	}
	XmStringFree(xs);
}


/*
 * dbprog_set_seg_title
 *	Set the segment title label on the credits window
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing;
 */
STATIC void
dbprog_set_seg_title(void)
{
	XmString	xs;

	if (credits_mode != CREDITS_SEG)
		return;

	XtVaGetValues(widgets.segments.segment_lbl,
		XmNlabelString, &xs,
		NULL
	);
	XtVaSetValues(widgets.credits.disctrk_lbl,
		XmNlabelString, xs,
		NULL
	);
	XmStringFree(xs);

	xs = XmStringCreateLtoR(
		w_seg.name == NULL ? "<new segment>" :  w_seg.name,
		XmSTRING_DEFAULT_CHARSET
	);
	XtVaSetValues(widgets.credits.title_lbl,
		XmNlabelString, xs,
		NULL
	);
	XmStringFree(xs);
}


/*
 * dbprog_extt_autotrk_upd
 *	If auto-track is enabled, display the new track details and
 *	credits info.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	new_pos - The new track list position to go to
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_extt_autotrk_upd(curstat_t *s, int new_pos)
{
	if (auto_trk && new_pos > 0 && sel_pos != new_pos) {
		/* Make track details window go to the new track */
		if (sel_pos > 0) {
			XmListDeselectPos(widgets.dbprog.trk_list, sel_pos);
			sel_pos = -1;
		}
		XmListSelectPos(widgets.dbprog.trk_list, new_pos, True);

		/* Scroll track list if necessary */
		dbprog_list_autoscroll(
			widgets.dbprog.trk_list,
			(int) s->tot_trks,
			new_pos
		);
	}
}


/*
 * dbprog_curgenre
 *	Return genre structure pointers for the current genre setting.
 *
 * Args:
 *	trkpos - -1 for the album genre information, or track position
 *		 (0 based) for genre information about a track.
 *	genrep - Return structure pointer for the primary genre.
 *	subgenrep - Return structure pointer for the primary subgenre.
 *	genre2p - Return structure pointer for the secondary genre.
 *	subgenre2p - Return structure pointer for the secondary subgenre.
 *
 *	Each of the return pointers may be set to NULL if no genre is
 *	currently set.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_curgenre(
	int		trkpos,
	cdinfo_genre_t	**genrep,
	cdinfo_genre_t	**subgenrep,
	cdinfo_genre_t	**genre2p,
	cdinfo_genre_t	**subgenre2p
)
{
	cdinfo_genre_t	*p,
			*q;
	char		*genre,
			*genre2;

	*genrep = *subgenrep = *genre2p = *subgenre2p = NULL;

	if (trkpos == -1) {
		genre = dbp->disc.genre;
		genre2 = dbp->disc.genre2;
	}
	else {
		genre = dbp->track[trkpos].genre;
		genre2 = dbp->track[trkpos].genre2;
	}

	if (genre != NULL) {
		for (p = dbp->genrelist; p != NULL; p = p->next) {
			if (strcmp(p->id, genre) == 0) {
				*genrep = p;
				*subgenrep = NULL;
				break;
			}
			for (q = p->child; q != NULL; q = q->next) {
				if (strcmp(q->id, genre) == 0) {
					*genrep = q->parent;
					*subgenrep = q;
					break;
				}
			}
			if (*genrep != NULL)
				break;
		}
	}
	if (genre2 != NULL) {
		for (p = dbp->genrelist; p != NULL; p = p->next) {
			if (strcmp(p->id, genre2) == 0) {
				*genre2p = p;
				*subgenre2p = NULL;
				break;
			}
			for (q = p->child; q != NULL; q = q->next) {
				if (strcmp(q->id, genre2) == 0) {
					*genre2p = q->parent;
					*subgenre2p = q;
					break;
				}
			}
			if (*genre2p != NULL)
				break;
		}
	}
}


/*
 * dbprog_genreupd
 *	Update the genre selector menus to indicate the currently set
 *	genres.
 *
 * Args:
 *	trkpos - If -1, update the album genre widgets, or specify a
 *		 track position (0 based) to update the track genre
 *		 widgets.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_genreupd(int trkpos)
{
	int			i;
	Dimension		x;
	Arg			arg[10];
	Widget			w;
	cdinfo_genre_t		*p,
				*q,
				*sg,
				*sg2,
				*genrep,
				*subgenrep,
				*genre2p,
				*subgenre2p;
	static cdinfo_genre_t	*save_genre = NULL,
				*save_genre2 = NULL,
				*save_tgenre = NULL,
				*save_tgenre2 = NULL;
	static bool_t		first = TRUE;

	if (dbp->genrelist == NULL)
		return;

	if (first) {
		first = FALSE;

		/* Create primary genre menu entries */
		for (p = dbp->genrelist; p != NULL; p = p->next) {
			/* Create primary genre entries */
			i = 0;
			XtSetArg(arg[i], XmNinitialResourcesPersistent,
				 False); i++;
			XtSetArg(arg[i], XmNshadowThickness, 2); i++;
			p->aux = (void *) XmCreatePushButton(
				widgets.dbextd.genre_menu[0],
				p->name,
				arg,
				i
			);
			p->aux2 = (void *) XmCreatePushButton(
				widgets.dbextd.genre_menu[1],
				p->name,
				arg,
				i
			);
			p->aux3 = (void *) XmCreatePushButton(
				widgets.dbextt.genre_menu[0],
				p->name,
				arg,
				i
			);
			p->aux4 = (void *) XmCreatePushButton(
				widgets.dbextt.genre_menu[1],
				p->name,
				arg,
				i
			);

			XtManageChild((Widget) p->aux);
			XtManageChild((Widget) p->aux2);
			XtManageChild((Widget) p->aux3);
			XtManageChild((Widget) p->aux4);

			register_activate_cb((Widget) p->aux,
					     dbprog_genre_sel, p);
			register_activate_cb((Widget) p->aux2,
					     dbprog_genre_sel, p);
			register_activate_cb((Widget) p->aux3,
					     dbprog_genre_sel, p);
			register_activate_cb((Widget) p->aux4,
					     dbprog_genre_sel, p);
		}
	}

	/* Find current genre and subgenre entries */
	dbprog_curgenre(trkpos, &genrep, &subgenrep, &genre2p, &subgenre2p);

	if (trkpos == -1) {
		sg = save_genre;
		sg2 = save_genre2;
	}
	else {
		sg = save_tgenre;
		sg2 = save_tgenre2;
	}

	/* If the primary genre changed, re-create the associated
	 * sub-genre menu entries
	 */
	if (sg != NULL &&
	    (genrep == NULL || subgenrep == NULL || genrep != sg)) {
		for (q = sg->child; q != NULL; q = q->next) {
			/* Delete old sub-genre entries */
			if (trkpos == -1) {
				if (q->aux != NULL) {
					XtUnmanageChild((Widget) q->aux);
					XtDestroyWidget((Widget) q->aux);
					q->aux = NULL;
				}
			}
			else {
				if (q->aux3 != NULL) {
					XtUnmanageChild((Widget) q->aux3);
					XtDestroyWidget((Widget) q->aux3);
					q->aux3 = NULL;
				}
			}
		}
	}
	if (sg2 != NULL &&
	    (genre2p == NULL || subgenre2p == NULL || genre2p != sg2)) {
		for (q = sg2->child; q != NULL; q = q->next) {
			/* Delete old sub-genre entries */
			if (trkpos == -1) {
				if (q->aux2 != NULL) {
					XtUnmanageChild((Widget) q->aux2);
					XtDestroyWidget((Widget) q->aux2);
					q->aux2 = NULL;
				}
			}
			else {
				if (q->aux4 != NULL) {
					XtUnmanageChild((Widget) q->aux4);
					XtDestroyWidget((Widget) q->aux4);
					q->aux4 = NULL;
				}
			}
		}
	}

	if (genrep != NULL && genrep != sg) {
		for (q = genrep->child; q != NULL; q = q->next) {
			/* Create new sub-genre entries */
			i = 0;
			XtSetArg(arg[i], XmNinitialResourcesPersistent,
				 False); i++;
			XtSetArg(arg[i], XmNshadowThickness, 2); i++;
			if (trkpos == -1) {
				q->aux = (void *) XmCreatePushButton(
					widgets.dbextd.subgenre_menu[0],
					q->name,
					arg,
					i
				);
				XtManageChild((Widget) q->aux);
				register_activate_cb((Widget) q->aux,
						     dbprog_subgenre_sel, q);
			}
			else {
				q->aux3 = (void *) XmCreatePushButton(
					widgets.dbextt.subgenre_menu[0],
					q->name,
					arg,
					i
				);
				XtManageChild((Widget) q->aux3);
				register_activate_cb((Widget) q->aux3,
						     dbprog_subgenre_sel, q);
			}
		}
	}

	if (genre2p != NULL && genre2p != sg2) {
		for (q = genre2p->child; q != NULL; q = q->next) {
			/* Create new sub-genre entries */
			i = 0;
			XtSetArg(arg[i], XmNinitialResourcesPersistent,
				 False); i++;
			XtSetArg(arg[i], XmNshadowThickness, 2); i++;
			if (trkpos == -1) {
				q->aux2 = (void *) XmCreatePushButton(
					widgets.dbextd.subgenre_menu[1],
					q->name,
					arg,
					i
				);
				XtManageChild((Widget) q->aux2);
				register_activate_cb((Widget) q->aux2,
						     dbprog_subgenre_sel, q);
			}
			else {
				q->aux4 = (void *) XmCreatePushButton(
					widgets.dbextt.subgenre_menu[1],
					q->name,
					arg,
					i
				);
				XtManageChild((Widget) q->aux4);
				register_activate_cb((Widget) q->aux4,
						     dbprog_subgenre_sel, q);
			}
		}
	}

	if (genrep != NULL) {
		if (trkpos == -1) {
			if (genrep->aux != NULL)
				XtVaSetValues(widgets.dbextd.genre_opt[0],
					XmNmenuHistory, genrep->aux,
					NULL
				);
		}
		else {
			if (genrep->aux3 != NULL)
				XtVaSetValues(widgets.dbextt.genre_opt[0],
					XmNmenuHistory, genrep->aux3,
					NULL
				);
		}
	}
	else {
		if (trkpos == -1) {
			XtVaSetValues(widgets.dbextd.genre_opt[0],
				XmNmenuHistory,
				widgets.dbextd.genre_none_btn[0],
				NULL
			);
		}
		else {
			XtVaSetValues(widgets.dbextt.genre_opt[0],
				XmNmenuHistory,
				widgets.dbextt.genre_none_btn[0],
				NULL
			);
		}
	}

	if (subgenrep != NULL) {
		if (trkpos == -1) {
			if (subgenrep->aux != NULL)
				XtVaSetValues(widgets.dbextd.subgenre_opt[0],
					XmNmenuHistory, subgenrep->aux,
					NULL
				);
		}
		else {
			if (subgenrep->aux3 != NULL)
				XtVaSetValues(widgets.dbextt.subgenre_opt[0],
					XmNmenuHistory, subgenrep->aux3,
					NULL
				);
		}
	}
	else {
		if (trkpos == -1) {
			XtVaSetValues(widgets.dbextd.subgenre_opt[0],
				XmNmenuHistory,
				widgets.dbextd.subgenre_none_btn[0],
				NULL
			);
		}
		else {
			XtVaSetValues(widgets.dbextt.subgenre_opt[0],
				XmNmenuHistory,
				widgets.dbextt.subgenre_none_btn[0],
				NULL
			);
		}
	}

	if (genre2p != NULL) {
		if (trkpos == -1) {
			if (genre2p->aux2 != NULL)
				XtVaSetValues(widgets.dbextd.genre_opt[1],
					XmNmenuHistory, genre2p->aux2,
					NULL
				);
		}
		else {
			if (genre2p->aux4 != NULL)
				XtVaSetValues(widgets.dbextt.genre_opt[1],
					XmNmenuHistory, genre2p->aux4,
					NULL
				);
		}
	}
	else {
		if (trkpos == -1) {
			XtVaSetValues(widgets.dbextd.genre_opt[1],
				XmNmenuHistory,
				widgets.dbextd.genre_none_btn[1],
				NULL
			);
		}
		else {
			XtVaSetValues(widgets.dbextt.genre_opt[1],
				XmNmenuHistory,
				widgets.dbextt.genre_none_btn[1],
				NULL
			);
		}
	}

	if (subgenre2p != NULL) {
		if (trkpos == -1) {
			if (subgenre2p->aux2 != NULL)
				XtVaSetValues(widgets.dbextd.subgenre_opt[1],
					XmNmenuHistory, subgenre2p->aux2,
					NULL
				);
		}
		else {
			if (subgenre2p->aux4 != NULL)
				XtVaSetValues(widgets.dbextt.subgenre_opt[1],
					XmNmenuHistory, subgenre2p->aux4,
					NULL
				);
		}
	}
	else {
		if (trkpos == -1) {
			XtVaSetValues(widgets.dbextd.subgenre_opt[1],
				XmNmenuHistory,
				widgets.dbextd.subgenre_none_btn[1],
				NULL
			);
		}
		else {
			XtVaSetValues(widgets.dbextt.subgenre_opt[1],
				XmNmenuHistory,
				widgets.dbextt.subgenre_none_btn[1],
				NULL
			);
		}
	}

	if (trkpos == -1) {
		save_genre = genrep;
		save_genre2 = genre2p;
		w = widgets.dbextd.form;
	}
	else {
		save_tgenre = genrep;
		save_tgenre2 = genre2p;
		w = widgets.dbextt.form;
	}

	if (genrep != NULL || genre2p != NULL) {
		/* Hack: Force the subgenre option menu to resize to
		 * match the selected label.
		 */
		XtVaGetValues(w, XmNwidth, &x, NULL);
		XtVaSetValues(w, XmNwidth, x+1, NULL);
		XtVaSetValues(w, XmNwidth, x, NULL);
	}
}


/*
 * dbprog_regionupd
 *	Update the region display to match the current region setting.
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_regionupd(void)
{
	cdinfo_region_t	*rp;
	XmString	xs;
	int		i;
	static bool_t	first = TRUE;

	if (dbp->regionlist == NULL) {
		XtSetSensitive(widgets.dbextd.region_chg_btn, False);
		XtSetSensitive(widgets.userreg.region_chg_btn, False);
		return;
	}

	if (first) {
		first = FALSE;
		/* Set up region selector */
		for (i = 1, rp = dbp->regionlist; rp != NULL;
		     i++, rp = rp->next) {
			xs = XmStringCreate(rp->name, CHSET1);
			XmListAddItemUnselected(widgets.regionsel.region_list,
						xs, i);
			XmStringFree(xs);
		}
		reg_cnt = i - 1;

		if (reg_cnt > 0) {
			XtSetSensitive(widgets.dbextd.region_chg_btn, True);
			XtSetSensitive(widgets.userreg.region_chg_btn, True);
		}
	}

	if (dbp->disc.region != NULL)
		XmTextSetString(widgets.dbextd.region_txt,
				cdinfo_region_name(dbp->disc.region));
	else
		XmTextSetString(widgets.dbextd.region_txt, "");
}


/*
 * dbprog_creditupd
 *	Update the credits window widgets
 *
 * Args:
 *	s - Pointer to the curstat_t structure
 *	pos - Track position (0-based) for track credit, or -1 for
 *	      album credit.
 *
 * Return:
 *	Nothing
 */
STATIC void
dbprog_creditupd(curstat_t *s, int pos)
{
	int			i;
	Arg			arg[10];
	cdinfo_role_t		*p;
	cdinfo_credit_t		*q;
	char			*str;
	XmString		xs;
	static bool_t		first = TRUE;

	if (dbp->rolelist != NULL && first) {
		first = FALSE;

		/* Create primary role menu entries */
		for (p = dbp->rolelist; p != NULL; p = p->next) {
			/* Create primary role entries */
			i = 0;
			XtSetArg(arg[i], XmNinitialResourcesPersistent,
				 False); i++;
			XtSetArg(arg[i], XmNshadowThickness, 2); i++;
			p->aux = (void *) XmCreatePushButton(
				widgets.credits.prirole_menu,
				p->name,
				arg,
				i
			);
			XtManageChild((Widget) p->aux);
			register_activate_cb((Widget) p->aux,
					     dbprog_role_sel, p);
		}
	}

	switch (credits_mode) {
	case CREDITS_DISC:
		q = dbp->disc.credit_list;
		dbprog_set_disc_title(s->cur_disc,
				      dbp->disc.artist,
				      dbp->disc.title);
		break;

	case CREDITS_TRACK:
		if (pos < 0)
			return;	/* Invalid track */
		q = dbp->track[pos].credit_list;
		dbprog_set_track_title(
			s->trkinfo[pos].trkno,
			dbp->track[pos].title
		);
		break;

	case CREDITS_SEG:
		q = w_seg.credit_list;
		dbprog_set_seg_title();
		break;

	default:
		return;	/* Invalid mode */
	}

	/* Update credits list widget */
	XmListDeleteAllItems(widgets.credits.cred_list);
	cred_pos = -1;

	for (i = 1; q != NULL; q = q->next, i++) {
		char	*rolename,
			*name;

		rolename = q->crinfo.role == NULL ? "unknown" :
				cdinfo_role_name(q->crinfo.role->id);
		name = q->crinfo.name == NULL ? "unknown" : q->crinfo.name;

		str = (char *) MEM_ALLOC("credlist_ent",
			strlen(rolename) + strlen(name) + 8
		);
		if (str == NULL) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
		(void) sprintf(str, CREDITLIST_FMT,
			       name, rolename,
			       q->notes != NULL ? ASTERISK_STR : "");

		xs = XmStringCreate(str, CHSET1);
		XmListAddItemUnselected(widgets.credits.cred_list, xs, i);
		XmStringFree(xs);

		MEM_FREE(str);
	}

	/* Set role selectors to "None" */
	XtVaSetValues(
		widgets.credits.prirole_opt,
		XmNmenuHistory, widgets.credits.prirole_none_btn,
		NULL
	);
	XtCallCallbacks(
		widgets.credits.prirole_none_btn,
		XmNactivateCallback, (XtPointer) NULL
	);

	w_cred.crinfo.role = NULL;

	/* Set fields to null */
	/* Explicitly set fullname fields */
	if (w_cred.crinfo.fullname.dispname != NULL) {
		MEM_FREE(w_cred.crinfo.fullname.dispname);
		w_cred.crinfo.fullname.dispname = NULL;
	}
	if (w_cred.crinfo.fullname.lastname != NULL) {
		MEM_FREE(w_cred.crinfo.fullname.lastname);
		w_cred.crinfo.fullname.lastname = NULL;
	}
	if (w_cred.crinfo.fullname.firstname != NULL) {
		MEM_FREE(w_cred.crinfo.fullname.firstname);
		w_cred.crinfo.fullname.firstname = NULL;
	}
	/* The fields are set via callback for these */
	XmTextSetString(widgets.credits.name_txt, "");
	XmTextSetString(widgets.credits.notes_txt, "");

	if (fname_mode == FNAME_CREDITS) {
		/* Update fullname window: note that call_data is passed
		 * a NULL pointer.  The dbprog_fullname function doesn't
		 * use that field currently, but if that is changed in the
		 * future this will have to change to match.
		 */
		fname_changed = FALSE;
		dbprog_fullname(
			widgets.credits.fullname_btn,
			(XtPointer) FALSE, NULL
		);
	}

	XtSetSensitive(widgets.credits.add_btn, False);
	XtSetSensitive(widgets.credits.del_btn, False);
	XtSetSensitive(widgets.credits.mod_btn, False);

	switch (credits_mode) {
	case CREDITS_DISC:
		dbprog_dcred_lblupd();
		break;
	case CREDITS_TRACK:
		dbprog_tcred_lblupd(&dbp->track[pos]);
		break;
	case CREDITS_SEG:
		dbprog_scred_lblupd();
		break;
	default:
		break;
	}
}


/*
 * dbprog_credit_ck
 *	Perform sanity checking of a credit structure
 *
 * Args:
 *	p - Pointer to the credit structure to be checked
 *	pos - Track position (0-based) for the credit, or -1 for album credit
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
STATIC bool_t
dbprog_credit_ck(cdinfo_credit_t *cp, int pos, curstat_t *s)
{
	cdinfo_credit_t	*list,
			*p;
	char		buf[16],
			*str;
	int		i,
			j;

	if (cp->crinfo.role == NULL) {
		CD_INFO(app_data.str_needrole);
		return FALSE;
	}
	if (cp->crinfo.name == NULL) {
		CD_INFO(app_data.str_needrolename);
		return FALSE;
	}

	switch (credits_mode) {
	case CREDITS_DISC:
		list = dbp->disc.credit_list;
		break;
	case CREDITS_TRACK:
		if (pos < 0)
			return FALSE;
		list = dbp->track[pos].credit_list;
		break;
	case CREDITS_SEG:
		list = w_seg.credit_list;
		break;
	default:
		return FALSE;
	}

	/* Check to make sure this credit is not a duplicate of an
	 * another credit in the same list
	 */
	i = 0;
	for (p = list; p != NULL; p = p->next) {
		if (cred_pos > 0 && ++i == cred_pos)
			continue; /* Don't check the one being modified */

		if (cp->crinfo.role == p->crinfo.role &&
		    strcmp(cp->crinfo.name, p->crinfo.name) == 0) {
			if (cp->notes != NULL && p->notes != NULL) {
			    if (strcmp(cp->notes, p->notes) == 0) {
				CD_INFO(app_data.str_dupcredit);
				return FALSE;
			    }
			}
			else if (cp->notes == NULL && p->notes == NULL) {
			    CD_INFO(app_data.str_dupcredit);
			    return FALSE;
			}
		}
	}

	if (credits_mode == CREDITS_DISC) {
		/* Check to make sure that the credit is not a duplicate
		 * of a track credit
		 */
		for (j = 0; j < (int) s->tot_trks; j++) {
		    for (p = dbp->track[j].credit_list; p != NULL;
			 p = p->next) {
			if (cp->crinfo.role == p->crinfo.role &&
			    p->crinfo.name != NULL &&
			    strcmp(cp->crinfo.name, p->crinfo.name) == 0) {
			    if (cp->notes != NULL && p->notes != NULL) {
				if (strcmp(cp->notes, p->notes) == 0) {
				    str = (char *) MEM_ALLOC(
					"str_duptrkcredit",
					strlen(app_data.str_duptrkcredit) + 8
				    );
				    (void) sprintf(buf, "%d",
						   s->trkinfo[j].trkno);
				    (void) sprintf(str,
						   app_data.str_duptrkcredit,
						   buf);
				    CD_INFO(str);
				    MEM_FREE(str);
				    return FALSE;
				}
			    }
			    else if (cp->notes == NULL && p->notes == NULL) {
				str = (char *) MEM_ALLOC(
				    "str_duptrkcredit",
				    strlen(app_data.str_duptrkcredit) + 8
				);
				(void) sprintf(buf, "%d", s->trkinfo[j].trkno);
				(void) sprintf(str, app_data.str_duptrkcredit,
					       buf);
				CD_INFO(str);
				MEM_FREE(str);
				return FALSE;
			    }
			}
		    }
		}
	}
	else if (credits_mode == CREDITS_TRACK) {
		/* Check to make sure the credit is not a duplicate of
		 * a disc credit
		 */
		for (p = dbp->disc.credit_list; p != NULL; p = p->next) {
		    if (cp->crinfo.role == p->crinfo.role &&
			p->crinfo.name != NULL &&
			strcmp(cp->crinfo.name, p->crinfo.name) == 0) {
			if (cp->notes != NULL && p->notes != NULL) {
			    if (strcmp(cp->notes, p->notes) == 0) {
				CD_INFO(app_data.str_dupdisccredit);
				return FALSE;
			    }
			}
			else if (cp->notes == NULL && p->notes == NULL) {
			    CD_INFO(app_data.str_dupdisccredit);
			    return FALSE;
			}
		    }
		}
	}

	return TRUE;
}


/*
 * dbprog_segmentupd
 *	Update the segments window widgets
 *
 * Args:
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	Nothing
 */
STATIC void
dbprog_segmentupd(curstat_t *s)
{
	int			i;
	cdinfo_segment_t	*q;
	cdinfo_credit_t		*cp,
				*cp2;
	char			*str;
	XmString		xs;

	if (!XtIsManaged(widgets.segments.form))
		/* No need to do anything now */
		return;

	/* Set credits window title if appropriate */
	dbprog_set_seg_title();

	/* Update segment window set button mode */
	dbprog_segments_setmode(s);

	/* Update segments list widget */
	XmListDeleteAllItems(widgets.segments.seg_list);
	seg_pos = -1;

	for (q = dbp->disc.segment_list, i = 1; q != NULL; q = q->next, i++) {
		char	*segname,
			*st,
			*sf,
			*et,
			*ef;

		segname = q->name == NULL ? "unknown" : q->name;
		st = q->start_track == NULL ? "??" : q->start_track;
		sf = q->start_frame == NULL ? "?" : q->start_frame;
		et = q->end_track == NULL ? "??" : q->end_track;
		ef = q->end_frame == NULL ? "?" : q->end_frame;

		str = (char *) MEM_ALLOC("seglist_ent",
			strlen(segname) + strlen(st) + strlen(sf) +
			strlen(et) + strlen(ef) + 8
		);
		if (str == NULL) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
		(void) sprintf(str, SEGLIST_FMT,
			       segname, st, sf, et, ef,
			       (q->notes != NULL || q->credit_list != NULL) ?
					ASTERISK_STR : "");

		xs = XmStringCreate(str, CHSET1);
		XmListAddItemUnselected(widgets.segments.seg_list, xs, i);
		XmStringFree(xs);

		MEM_FREE(str);
	}

	/* Set fields to null.  The fields are set via callback for these */
	XmTextSetString(widgets.segments.name_txt, "");
	XmTextSetString(widgets.segments.starttrk_txt, "");
	XmTextSetString(widgets.segments.startfrm_txt, "");
	XmTextSetString(widgets.segments.endtrk_txt, "");
	XmTextSetString(widgets.segments.endfrm_txt, "");
	XmTextSetString(widgets.segments.notes_txt, "");

	/* Clear credit list */
	for (cp = cp2 = w_seg.credit_list; cp != NULL; cp = cp2) {
		cp2 = cp->next;

		if (cp->crinfo.name != NULL)
			MEM_FREE(cp->crinfo.name);
		if (cp->crinfo.fullname.dispname != NULL)
			MEM_FREE(cp->crinfo.fullname.dispname);
		if (cp->crinfo.fullname.lastname != NULL)
			MEM_FREE(cp->crinfo.fullname.lastname);
		if (cp->crinfo.fullname.firstname != NULL)
			MEM_FREE(cp->crinfo.fullname.firstname);
		if (cp->crinfo.fullname.the != NULL)
			MEM_FREE(cp->crinfo.fullname.the);
		if (cp->notes != NULL)
			MEM_FREE(cp->notes);
		MEM_FREE(cp);
	}
	w_seg.credit_list = NULL;

	XtSetSensitive(widgets.segments.add_btn, False);
	XtSetSensitive(widgets.segments.del_btn, False);
	XtSetSensitive(widgets.segments.mod_btn, False);

	/* Update segments button label if needed */
	dbprog_seg_lblupd();

	s->segplay = SEGP_NONE;
	dpy_progmode(s, FALSE);
}


/*
 * dbprog_segment_ck
 *	Perform sanity checking of a segment structure
 *
 * Args:
 *	p - Pointer to the segment structure to be checked
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
STATIC bool_t
dbprog_segment_ck(cdinfo_segment_t *cp, curstat_t *s)
{
	int		i;
	sword32_t	st,
			et,
			sf,
			ef,
			startaddr,
			endaddr;

	if (cp->name == NULL ||
	    cp->start_track == NULL || cp->start_frame == NULL ||
	    cp->end_track == NULL || cp->end_frame == NULL) {
		CD_INFO(app_data.str_incseginfo);
		return FALSE;
	}

	st = atoi(cp->start_track);
	et = atoi(cp->end_track);
	sf = atoi(cp->start_frame);
	ef = atoi(cp->end_frame);

	/* Check start track and frame numbers */
	for (i = 0; i < (int) s->tot_trks; i++) {
		if (s->trkinfo[i].trkno == st)
			break;
	}
	if (i == s->tot_trks ||
	    sf >= (s->trkinfo[i+1].addr - s->trkinfo[i].addr)) {
		CD_INFO(app_data.str_invseginfo);
		return FALSE;
	}
	startaddr = s->trkinfo[i].addr + sf;

	/* Check end track and frame numbers */
	for (i = 0; i < (int) s->tot_trks; i++) {
		if (s->trkinfo[i].trkno == et)
			break;
	}
	if (i == s->tot_trks ||
	    ef >= (s->trkinfo[i+1].addr - s->trkinfo[i].addr)) {
		CD_INFO(app_data.str_invseginfo);
		return FALSE;
	}
	endaddr = s->trkinfo[i].addr + ef;

	/* Check start < end */
	if (endaddr <= (startaddr + app_data.min_playblks)) {
		CD_INFO(app_data.str_segposerr);
		return FALSE;
	}

	return TRUE;
}


/*
 * dbprog_extdupd
 *	Update the state of the disc details window widgets to match that
 *	of the cdinfo_incore_t structure.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_extdupd(curstat_t *s)
{
	char		*p,
			discid[16];
	XmString	xs;

	/* Sort title */
	if (dbp->disc.sorttitle != NULL) {
		p = XmTextGetString(widgets.dbextd.sorttitle_txt);
		if (p == NULL || strcmp(p, dbp->disc.sorttitle) != 0) {
			XmTextSetString(widgets.dbextd.sorttitle_txt,
					dbp->disc.sorttitle);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextd.sorttitle_txt, "");

	/* Title "The" */
	if (dbp->disc.title_the != NULL) {
		p = XmTextGetString(widgets.dbextd.the_txt);
		if (p == NULL || strcmp(p, dbp->disc.title_the) != 0) {
			XmTextSetString(widgets.dbextd.the_txt,
					dbp->disc.title_the);
			XmToggleButtonSetState(widgets.dbextd.the_btn,
						True, False);
			XtSetSensitive(widgets.dbextd.the_txt, True);
		}
		if (p != NULL)
			XtFree(p);
	}
	else {
		XmTextSetString(widgets.dbextd.the_txt, "");
		XmToggleButtonSetState(widgets.dbextd.the_btn, False, False);
		XtSetSensitive(widgets.dbextd.the_txt, False);
	}

	/* Year */
	if (dbp->disc.year != NULL) {
		p = XmTextGetString(widgets.dbextd.year_txt);
		if (p == NULL || strcmp(p, dbp->disc.year) != 0) {
			XmTextSetString(widgets.dbextd.year_txt,
					dbp->disc.year);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextd.year_txt, "");

	/* Label */
	if (dbp->disc.label != NULL) {
		p = XmTextGetString(widgets.dbextd.label_txt);
		if (p == NULL || strcmp(p, dbp->disc.label) != 0) {
			XmTextSetString(widgets.dbextd.label_txt,
					dbp->disc.label);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextd.label_txt, "");

	/* Compilation */
	XmToggleButtonSetState(widgets.dbextd.comp_btn,
			       dbp->disc.compilation, False);

	/* Genres and subgenres */
	dbprog_genreupd(-1);

	/* Disc number in set */
	if (dbp->disc.dnum != NULL) {
		p = XmTextGetString(widgets.dbextd.dnum_txt);
		if (p == NULL || strcmp(p, dbp->disc.dnum) != 0)
			XmTextSetString(widgets.dbextd.dnum_txt,
					dbp->disc.dnum);
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextd.dnum_txt, "");

	/* Total discs in set */
	if (dbp->disc.tnum != NULL) {
		p = XmTextGetString(widgets.dbextd.tnum_txt);
		if (p == NULL || strcmp(p, dbp->disc.tnum) != 0)
			XmTextSetString(widgets.dbextd.tnum_txt,
					dbp->disc.tnum);
	}
	else
		XmTextSetString(widgets.dbextd.tnum_txt, "");

	/* Album Region */
	dbprog_regionupd();

	/* Disc notes */
	if (dbp->disc.notes != NULL) {
		p = XmTextGetString(widgets.dbextd.notes_txt);
		if (p == NULL || strcmp(p, dbp->disc.notes) != 0)
			XmTextSetString(widgets.dbextd.notes_txt,
					dbp->disc.notes);
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextd.notes_txt, "");

	/* Disc ID */
	if (dbp->discid != 0)
		(void) sprintf(discid, "%08x", dbp->discid);
	else
		discid[0] = '\0';
	xs = XmStringCreateSimple(discid);
	XtVaSetValues(widgets.dbextd.discid_ind,
		XmNlabelString, xs,
		NULL
	);
	XmStringFree(xs);

	/* Revision */
	xs = XmStringCreateSimple(
		dbp->disc.revision == NULL ? "-" : dbp->disc.revision
	);
	XtVaSetValues(widgets.dbextd.rev_ind,
		XmNlabelString, xs,
		NULL
	);
	XmStringFree(xs);

	/* Certifier */
	xs = XmStringCreateSimple(
		dbp->disc.certifier == NULL ? "-" : dbp->disc.certifier
	);
	XtVaSetValues(widgets.dbextd.cert_ind,
		XmNlabelString, xs,
		NULL
	);
	XmStringFree(xs);

	/* Update disc details window and credits window headings */
	dbprog_set_disc_title(s->cur_disc, dbp->disc.artist, dbp->disc.title);
	dbprog_set_seg_title();

	/* Update button labels */
	dbprog_extd_lblupd();
	dbprog_dcred_lblupd();
	dbprog_seg_lblupd();
}


/*
 * dbprog_exttupd
 *	Update the state of the track details window widgets to match
 *	that of the cdinfo_incore_t structure for the specified track.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	trkpos - Track position.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_exttupd(curstat_t *s, int trkpos)
{
	char		*p;
	XmString	xs;

	/* Set track details track title */
	dbprog_set_track_title(
		s->trkinfo[trkpos].trkno,
		dbp->track[trkpos].title
	);

	/* Sort title */
	if (dbp->track[trkpos].sorttitle != NULL) {
		p = XmTextGetString(widgets.dbextt.sorttitle_txt);
		if (p == NULL || strcmp(p, dbp->track[trkpos].sorttitle) != 0){
			XmTextSetString(
				widgets.dbextt.sorttitle_txt,
				dbp->track[trkpos].sorttitle
			);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextt.sorttitle_txt, "");

	/* Title "The" */
	if (dbp->track[trkpos].title_the != NULL) {
		p = XmTextGetString(widgets.dbextt.the_txt);
		if (p == NULL || strcmp(p, dbp->track[trkpos].title_the) != 0){
			XmTextSetString(widgets.dbextt.the_txt,
					dbp->track[trkpos].title_the);
			XmToggleButtonSetState(widgets.dbextt.the_btn,
						True, False);
			XtSetSensitive(widgets.dbextt.the_txt, True);
		}
		if (p != NULL)
			XtFree(p);
	}
	else {
		XmTextSetString(widgets.dbextt.the_txt, "");
		XmToggleButtonSetState(widgets.dbextt.the_btn, False, False);
		XtSetSensitive(widgets.dbextt.the_txt, False);
	}

	/* Artist */
	if (dbp->track[trkpos].artist != NULL) {
		p = XmTextGetString(widgets.dbextt.artist_txt);
		if (p == NULL || strcmp(p, dbp->track[trkpos].artist) != 0) {
			XmTextSetString(
				widgets.dbextt.artist_txt,
				dbp->track[trkpos].artist
			);
			XtVaSetValues(widgets.dbextt.artist_txt,
				XmNcursorPosition, 0,
				NULL
			);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextt.artist_txt, "");

	/* Year */
	if (dbp->track[trkpos].year != NULL) {
		p = XmTextGetString(widgets.dbextt.year_txt);
		if (p == NULL || strcmp(p, dbp->track[trkpos].year) != 0) {
			XmTextSetString(
				widgets.dbextt.year_txt,
				dbp->track[trkpos].year
			);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextt.year_txt, "");

	/* Label */
	if (dbp->track[trkpos].label != NULL) {
		p = XmTextGetString(widgets.dbextt.label_txt);
		if (p == NULL || strcmp(p, dbp->track[trkpos].label) != 0) {
			XmTextSetString(
				widgets.dbextt.label_txt,
				dbp->track[trkpos].label
			);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextt.label_txt, "");

	/* BPM */
	if (dbp->track[trkpos].bpm != NULL) {
		p = XmTextGetString(widgets.dbextt.bpm_txt);
		if (p == NULL || strcmp(p, dbp->track[trkpos].bpm) != 0) {
			XmTextSetString(
				widgets.dbextt.bpm_txt,
				dbp->track[trkpos].bpm
			);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextt.bpm_txt, "");

	/* Genres and subgenres */
	dbprog_genreupd(trkpos);

	/* Track notes */
	if (dbp->track[trkpos].notes != NULL) {
		p = XmTextGetString(widgets.dbextt.notes_txt);
		if (p == NULL || strcmp(p, dbp->track[trkpos].notes) != 0) {
			XmTextSetString(
				widgets.dbextt.notes_txt,
				dbp->track[trkpos].notes
			);
		}
		if (p != NULL)
			XtFree(p);
	}
	else
		XmTextSetString(widgets.dbextt.notes_txt, "");

	/* ISRC */
	xs = XmStringCreateSimple(
		dbp->track[trkpos].isrc == NULL ? "-" : dbp->track[trkpos].isrc
	);
	XtVaSetValues(widgets.dbextt.isrc_ind,
		XmNlabelString, xs,
		NULL
	);
	XmStringFree(xs);
}


/*
 * dbprog_structupd
 *	Update the state of the various widgets fields in the
 *	CD info/program window to match that of the cdinfo_incore_t structure.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_structupd(curstat_t *s)
{
	char	*p;

	/* Total time */
	dbprog_dpytottime(s);

	/* Disc artist */
	if (dbp->disc.artist != NULL) {
		p = XmTextGetString(widgets.dbprog.artist_txt);
		if (p == NULL || strcmp(p, dbp->disc.artist) != 0) {
			XmTextSetString(widgets.dbprog.artist_txt,
					dbp->disc.artist);
			XtVaSetValues(widgets.dbprog.artist_txt,
				XmNcursorPosition, 0,
				NULL
			);
		}
		if (p != NULL)
			XtFree(p);
	}
	else {
		XmTextSetString(widgets.dbprog.artist_txt, "");
	}

	/* Disc title */
	if (dbp->disc.title != NULL) {
		p = XmTextGetString(widgets.dbprog.title_txt);
		if (p == NULL || strcmp(p, dbp->disc.title) != 0) {
			XmTextSetString(widgets.dbprog.title_txt,
					dbp->disc.title);
		}
		if (p != NULL)
			XtFree(p);
	}
	else {
		XmTextSetString(widgets.dbprog.title_txt, "");
	}

	/* Track title list */
	sel_pos = -1;
	dbprog_listupd_all(s, TRUE);

	/* Program sequence */
	if (dbp->playorder != NULL && !s->onetrk_prog) {
		p = XmTextGetString(widgets.dbprog.pgmseq_txt);
		if (p == NULL || strcmp(p, dbp->playorder) != 0) {
			XmTextSetString(widgets.dbprog.pgmseq_txt,
					dbp->playorder);
			XmTextSetInsertionPosition(
				widgets.dbprog.pgmseq_txt,
				strlen(dbp->playorder)
			);
		}
		if (p != NULL)
			XtFree(p);
		XtSetSensitive(widgets.dbprog.clrpgm_btn, True);
	}
	else {
		XmTextSetString(widgets.dbprog.pgmseq_txt, "");
		XtSetSensitive(widgets.dbprog.clrpgm_btn, False);
		XtSetSensitive(widgets.dbprog.savepgm_btn, False);
	}

	/* Update the disc details window widgets */
	dbprog_extdupd(s);

	/* Update button labels */
	dbprog_extt_lblupd(NULL);
	dbprog_tcred_lblupd(NULL);

	/* Update segments window if needed */
	dbprog_segmentupd(s);

	/* Update credits window if needed */
	switch (credits_mode) {
	case CREDITS_DISC:
	case CREDITS_SEG:
		dbprog_creditupd(s, -1);
		break;
	case CREDITS_TRACK:
		dbprog_creditupd(s, extt_pos);
		break;
	default:
		break;
	}

	/* Note: extt, credits and fullname window widgets are updated when
	 * the user pops up these windows, repectively.
	 */

	dbp->flags &= ~CDINFO_CHANGED;
	XtSetSensitive(widgets.dbprog.submit_btn, False);
	XtSetSensitive(widgets.dbprog.addpgm_btn, False);
	XtSetSensitive(widgets.dbprog.fullname_btn, True);
	XtSetSensitive(widgets.dbprog.extd_btn, True);
	XtSetSensitive(widgets.dbprog.dcredits_btn, True);
	XtSetSensitive(widgets.dbprog.segments_btn, True);
	XtSetSensitive(widgets.dbprog.extt_btn, False);
	XtSetSensitive(widgets.dbprog.tcredits_btn, False);

	/* Update display */
	dpy_dbmode(s, FALSE);
}


/*
 * dbprog_do_submit_popup
 *	Pop up the CDDB submit prompt dialog.
 *
 * Args:
 *	None
 *
 * Return:
 *	Nothing
 */
STATIC void
dbprog_do_submit_popup(void)
{
	char	*msg;

	msg = (char *) MEM_ALLOC(
		"msg",
		strlen(app_data.str_chgsubmit) +
		(dbp->disc.artist == NULL ? 0 : strlen(dbp->disc.artist)) +
		(dbp->disc.title == NULL ? 0 : strlen(dbp->disc.title)) + 4
	);

	if (msg == NULL) {
		CD_FATAL(app_data.str_nomemory);
		return;
	}

	(void) sprintf(msg, app_data.str_chgsubmit,
		       dbp->disc.artist == NULL ?
				app_data.str_unknartist : dbp->disc.artist,
		       dbp->disc.title == NULL ?
				app_data.str_unkndisc : dbp->disc.title);

	/* Pop-up the dialog box */
	cd_confirm_popup(
		app_data.str_confirm, msg,
		dbprog_do_clear, (XtPointer) STAT_SUBMIT,
		dbprog_do_clear, (XtPointer) 0
	);

	MEM_FREE(msg);
}


/*
 * dbprog_dbsubmit
 *	Submit current CD info to CDDB server.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
STATIC bool_t
dbprog_dbsubmit(curstat_t *s)
{
	cdinfo_ret_t	ret;
	bool_t		err;

	/* Check fullname for correctness, if needed */
	if (!dbprog_fullname_ck(s))
		return FALSE;

	/* Change to the watch cursor */
	cd_busycurs(TRUE, CURS_ALL);

	/* Configure the wwwWarp menu */
	wwwwarp_sel_cfg(s);

	/* Submit the CD information */
	if ((ret = cdinfo_submit(s)) != 0) {
		DBGPRN(DBG_CDI)(errfp, "cdinfo_submit: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
	}

	/* Change to the normal cursor */
	cd_busycurs(FALSE, CURS_ALL);

	switch (CDINFO_GET_STAT(ret)) {
	case 0:
		if ((dbp->flags & CDINFO_NEEDREG) != 0) {
			err = TRUE;

			/* User not registered with CDDB:
			 * Pop up CDDB user registration dialog
			 */
			userreg_do_popup(s, TRUE);
		}
		else {
			err = FALSE;

			/* Clear changed flag */
			dbp->flags &= ~CDINFO_CHANGED;

			/* Make the submit button insensitive */
			XtSetSensitive(widgets.dbprog.submit_btn, False);

			CD_INFO_AUTO(app_data.str_submitok);
		}
		break;

	case SUBMIT_ERR:
	default:
		err = TRUE;

		CD_INFO(app_data.str_submiterr);
		break;
	}

	if (XtIsManaged(widgets.dbprog.form))
		/* Put focus on the OK button */
		XmProcessTraversal(widgets.dbprog.ok_btn, XmTRAVERSE_CURRENT);

	return (!err);
}


/*
 * dbprog_pgm_active
 *	Indicate whether a play program is currently defined.
 *
 * Args:
 *	None.
 *
 * Return:
 *	TRUE = program is active,
 *	FALSE = program is not active.
 */
STATIC bool_t
dbprog_pgm_active(void)
{
	return (dbp->playorder != NULL && dbp->playorder[0] != '\0');
}


/*
 * dbprog_hist_addent
 *	Add a new entry to the disc list widget in history mode.
 *
 * Args:
 *	hp - Pointer to the associated cdinfo_dlist_t structure.
 *	s - Pointer to the curstat_t structure.
 *	pos - The list position to add to.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
dbprog_hist_addent(cdinfo_dlist_t *hp, curstat_t *s, int pos)
{
	struct tm	*tm;
	XmString	xs;
	char		*cp,
			str[DLIST_BUF_SZ + STR_BUF_SZ];

	tm = localtime(&hp->time);
	cp = util_monname(tm->tm_mon);

	(void) sprintf(str, HISTLIST_FMT,
		cp,
		tm->tm_mday,
		tm->tm_hour,
		tm->tm_min,
		(hp->artist == NULL) ? "" : hp->artist,
		(hp->artist != NULL && hp->title != NULL) ? " / " : "",
		(hp->title == NULL) ? "-" : hp->title
	);

	xs = XmStringCreate(str, CHSET1);
	XmListAddItemUnselected(widgets.dlist.disc_list, xs, pos);
	XmStringFree(xs);

	/* Check against max history limit */
	if (++hist_cnt > app_data.cdinfo_maxhist) {
		XmListDeletePos(widgets.dlist.disc_list, hist_cnt);
		hist_cnt--;
	}
}


/*
 * dbprog_hist_addall
 *	Make the disc list widget display the in-core history list.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_hist_addall(curstat_t *s)
{
	int		i;
	cdinfo_dlist_t	*hp;

	/* Put the history list in the list widget */
	for (i = 1, hp = cdinfo_hist_list(); hp != NULL; hp = hp->next, i++) {
		dbprog_hist_addent(hp, s, i);

		if (i == 1)
			XtSetSensitive(widgets.dlist.delall_btn, True);
	}
}


/*
 * dbprog_hist_new
 *	Add current CD to the history list.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_hist_new(curstat_t *s)
{
	cdinfo_dlist_t	h1,
			h2,
			*hp;

	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY || s->chgrscan)
		/* Don't add to history if no disc or changer scanning */
		return;

	h1.device = s->curdev;
	h1.discno = (int) s->cur_disc;
	h1.type = (s->qmode != QMODE_MATCH) ? 0 :
		((dbp->flags & CDINFO_FROMLOC) ?
			CDINFO_DLIST_LOCAL : CDINFO_DLIST_REMOTE);
	h1.discid = dbp->discid;
	h1.genre = dbp->disc.genre;
	h1.artist = dbp->disc.artist;
	h1.title = dbp->disc.title;
	h1.time = time(NULL);

	/* Save a copy of the old entry for comparison later */
	if ((hp = cdinfo_hist_list()) != NULL)
		h2 = *hp;	/* Structure copy */
	else
		(void) memset(&h2, 0, sizeof(cdinfo_dlist_t));

	/* Add to in-core history list */
	(void) cdinfo_hist_addent(&h1, TRUE);

	/* If in history mode, add to disc list widget */
	if (dlist_mode == DLIST_HIST) {
		if (hist_initted && h1.discid != h2.discid) {
			/* Add entry to the list widget,
			 * if it's different than what's
			 * already there.
			 */
			dbprog_hist_addent(&h1, s, 1);
		}

		if (!XtIsSensitive(widgets.dlist.delall_btn))
			XtSetSensitive(widgets.dlist.delall_btn, True);
	}
}


/*
 * dbprog_chgr_scan_next
 *	Scan next slot of the CD changer.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_chgr_scan_next(curstat_t *s)
{
	scan_id = -1;

	if (scan_slot < 0)
		return;	/* Error */

	/* Skip forward to the next loaded slot */
	if (scan_slot > 0 && s->cur_disc > scan_slot)
		scan_slot = s->cur_disc;

	if (++scan_slot > app_data.numdiscs) {
		/* Done scanning */
		scan_slot = -1;
		dbprog_chgr_scan_stop(s);
		return;
	}

	/* Go to next slot */
	s->cur_disc = scan_slot;

	s->flags |= STAT_CHGDISC;

	/* Ask the user if the changed CD information
	 * should be submitted to CDDB.
	 */
	if (!dbprog_chgsubmit(s))
		return;

	s->flags &= ~STAT_CHGDISC;

	/* Do the disc change */
	di_chgdisc(s);

	/* Update display */
	dpy_dbmode(s, FALSE);
	dpy_playmode(s, FALSE);
}


/*
 * dbprog_chgr_addent
 *	Add or update an entry in the disc list widget in CD changer mode
 *
 * Args:
 *	cp - Pointer to the associated cdinfo_dlist_t structure.
 *	s - Pointer to the curstat_t structure.
 *	pos - List position to add to.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_chgr_addent(cdinfo_dlist_t *cp, curstat_t *s, int pos)
{
	XmString	xs;
	char		str[DLIST_BUF_SZ + STR_BUF_SZ];

	(void) sprintf(str, CHGRLIST_FMT,
		cp->discno,
		(cp->artist == NULL) ? "" : cp->artist,
		(cp->artist != NULL && cp->title != NULL) ? " / " : "",
		(cp->title == NULL) ? "-" : cp->title
	);

	if (pos == s->cur_disc)
		xs = XmStringCreate(str, CHSET2);
	else
		xs = XmStringCreate(str, CHSET1);

	if (cp->aux == NULL) {
		/* New entry */
		cp->aux = (void *) widgets.dlist.disc_list;
		XmListAddItemUnselected(widgets.dlist.disc_list, xs, pos);
	}
	else {
		/* Replace existing entry */
		XmListReplaceItemsPos(widgets.dlist.disc_list, &xs, 1, pos);
	}

	XmStringFree(xs);

	if (dlist_pos >= 0)
		/* This entry was previously selected */
		XmListSelectPos(widgets.dlist.disc_list, dlist_pos, False);
}


/*
 * dbprog_chgr_addall
 *	Make the disc list widget display the in-core CD changer list.
 *
 * Args:
 *	s- Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_chgr_addall(curstat_t *s)
{
	cdinfo_dlist_t	*cp;

	/* Put the CD changer list in the list widget */
	for (cp = cdinfo_chgr_list(); cp != NULL; cp = cp->next) {
		cp->aux = NULL;	/* Denote new entry */

		dbprog_chgr_addent(cp, s, cp->discno);
	}

	dbprog_list_autoscroll(
		widgets.dlist.disc_list,
		(int) app_data.numdiscs,
		s->cur_disc
	);
}


/*
 * dbprog_chgr_new
 *	Add current CD to the CD changer list.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
dbprog_chgr_new(curstat_t *s)
{
	int		i;
	cdinfo_dlist_t	ch,
			*cp;

	if (s->cur_disc == 0)
		return;

	ch.device = s->curdev;
	ch.discno = (int) s->cur_disc;
	ch.type = (s->qmode != QMODE_MATCH) ? 0 :
		((dbp->flags & CDINFO_FROMLOC) ?
			CDINFO_DLIST_LOCAL : CDINFO_DLIST_REMOTE);
	ch.discid = dbp->discid;
	ch.genre = dbp->disc.genre;
	ch.artist = dbp->disc.artist;
	ch.title = dbp->disc.title;
	ch.time = time(NULL);

	/* Add to in-core CD changer list */
	(void) cdinfo_chgr_addent(&ch);

	if (dlist_mode != DLIST_CHGR)
		return;

	/* If in changer mode, add to disc list widget */
	for (i = 1, cp = cdinfo_chgr_list(); cp != NULL; cp = cp->next, i++)
		dbprog_chgr_addent(cp, s, i);

	/* Scroll to make current disc visible if no item
	 * of the list is currently selected.
	 */
	if (dlist_pos < 0) {
		dbprog_list_autoscroll(
			widgets.dlist.disc_list,
			(int) app_data.numdiscs,
			s->cur_disc
		);
	}

	if (s->chgrscan && s->mode != MOD_NODISC && s->mode != MOD_BUSY) {
		/* Schedule for the next slot */
		scan_id = cd_timeout(
			1000,
			dbprog_chgr_scan_next,
			(byte_t *) s
		);
	}
}


/*
 * dbprog_matchsel_popup
 *	Pop up the match selector window.
 *
 * Args:
 *	s- Pointer to the curstat_t structure.
 *
 * Return:
 *	None.
 */
STATIC void
dbprog_matchsel_popup(curstat_t *s)
{
	int		i,
			genrestr_len;
	cdinfo_match_t	*mp;
	XmString	xs;
	char		*str,
			*genrestr;

	if (dbp->matchlist == NULL) {
		/* Shouldn't be calling this when matchlist is empty */
		cd_beep();
		return;
	}

	/* Build matchsel list entries */
	mp = dbp->matchlist;
	for (i = 1; mp != NULL; i++, mp = mp->next) {
		if (mp->genre != NULL) {
			genrestr = cdinfo_genre_name(mp->genre);
			genrestr_len = strlen(genrestr);
		}
		else {
			genrestr = NULL;
			genrestr_len = 0;
		}

		str = (char *) MEM_ALLOC("match_str",
			(mp->artist == NULL ?
				strlen(app_data.str_unknartist) :
				strlen(mp->artist)) +
			(mp->title == NULL ?
				strlen(app_data.str_unkndisc) :
				strlen(mp->title)) +
			genrestr_len + 8
		);
		if (str == NULL) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}

		(void) sprintf(str, MATCHLIST_FMT,
			mp->artist == NULL ?
				app_data.str_unknartist : mp->artist,
			mp->title == NULL ?
				app_data.str_unkndisc : mp->title,
			genrestr == NULL ? "" : "(",
			genrestr == NULL ? "" : genrestr,
			genrestr == NULL ? "" : ")"
		);

		xs = XmStringCreate(str, CHSET1);

		XmListAddItemUnselected(widgets.matchsel.matchsel_list, xs, i);

		XmStringFree(xs);
		MEM_FREE(str);
	}

	xs = XmStringCreate(
		app_data.str_noneofabove,
		XmSTRING_DEFAULT_CHARSET
	);

	XmListAddItemUnselected(widgets.matchsel.matchsel_list, xs, i);

	XmStringFree(xs);

	match_cnt = i;

	/* Configure the wwwWarp menu */
	wwwwarp_sel_cfg(s);

	XtSetSensitive(widgets.matchsel.ok_btn, False);

	/* The matchsel popup has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason for this
	 * is we want to avoid a screen glitch when we move the window
	 * in cd_dialog_setpos(), so we map the window afterwards.
	 */
	if (!XtIsManaged(widgets.matchsel.form)) {
		XtManageChild(widgets.matchsel.form);

		/* Set up dialog box position */
		cd_dialog_setpos(XtParent(widgets.matchsel.form));

		XtMapWidget(XtParent(widgets.matchsel.form));
	}
}


/*
 * dbprog_autoname
 *	Auto-generate the dispname of a fullname from the lastname,
 *	firstname and the components.
 *
 * Args:
 *	Pointer to the fullname structure.
 *
 * Return:
 *	The generated text string.  This string should be freed by the
 *	caller via MEM_FREE when done.
 */
STATIC char *
dbprog_autoname(cdinfo_fname_t *fnp)
{
	char	*str;

	str = (char *) MEM_ALLOC("dispname",
		(fnp->the == NULL ? 0 : (strlen(fnp->the) + 1)) +
		(fnp->lastname == NULL ? 0 : (strlen(fnp->lastname) + 1)) +
		(fnp->firstname == NULL ? 0 : (strlen(fnp->firstname) + 1)) + 1
	);
	if (str == NULL) {
		CD_FATAL(app_data.str_nomemory);
		return NULL;
	}
	str[0] = '\0';

	if (fnp->the != NULL)
		(void) sprintf(str, "%s ", fnp->the);
	if (fnp->firstname != NULL)
		(void) strcat(str, fnp->firstname);
	if (fnp->lastname != NULL)
		(void) sprintf(str, "%s%s%s",
			       str,
			       fnp->firstname == NULL ? "" : " ",
			       fnp->lastname);
	return (str);
}


/***********************
 *   public routines   *
 ***********************/


/*
 * dbprog_curfileupd
 *	Update the curr.XXX file to show the current disc status.
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_curfileupd(void)
{
	cdinfo_curfileupd();
}


/*
 * dbprog_curtrkupd
 *	Update the track list display to show the current playing
 *	track entry in bold font.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_curtrkupd(curstat_t *s)
{
	int		pos,
			list_pos;
	static int	sav_pos = -1;

	/* Update curfile */
	dbprog_curfileupd();

	if (sav_pos >= 0) {
		/* Update track list entry: un-highlight previous track */
		dbprog_listupd_ent(
			s, sav_pos, 
			(dbp->track[sav_pos].title != NULL) ?
				dbp->track[sav_pos].title : UNDEF_STR,
			FALSE
		);

		if (sel_pos == (sav_pos + 1))
			/* This item is previously selected */
			XmListSelectPos(widgets.dbprog.trk_list,
					sel_pos, False);
	}

	if (s->cur_trk <= 0 || s->mode == MOD_BUSY || s->mode == MOD_NODISC) {
		sav_pos = -1;
		return;
	}

	sav_pos = pos = di_curtrk_pos(s);
	list_pos = pos + 1;

	/* Update track list entry: highlight current track */
	dbprog_listupd_ent(
		s, sav_pos, 
		(dbp->track[sav_pos].title != NULL) ?
			dbp->track[sav_pos].title : UNDEF_STR,
		FALSE
	);

	/* If this item is previously selected, re-select it */
	if (sel_pos == list_pos)
		XmListSelectPos(widgets.dbprog.trk_list, sel_pos, False);
	else if (ind_pos == list_pos)
		XmListSelectPos(widgets.dbprog.trk_list, ind_pos, False);

	/* Auto-scroll the track list if the current track is not visible.
	 * No scrolling is done while a track list entry is selected
	 * or if a track title is being edited.
	 */
	if (sel_pos < 0 && ind_pos < 0) {
		dbprog_list_autoscroll(
			widgets.dbprog.trk_list,
			(int) s->tot_trks,
			list_pos
		);
	}

	dbprog_extt_autotrk_upd(s, list_pos);
}


/*
 * dbprog_progclear
 *	Clear program sequence.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_progclear(curstat_t *s)
{
	cdinfo_ret_t	ret;

	/* Delete track program file */
	if (!s->onetrk_prog && (ret = cdinfo_del_prog()) != 0) {
		DBGPRN(DBG_CDI)(errfp, "cdinfo_del_prog: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
	}

	if (dbp->playorder != NULL) {
		MEM_FREE(dbp->playorder);
		dbp->playorder = NULL;
	}

	s->prog_tot = 0;
	s->prog_cnt = 0;
	s->program = FALSE;

	/* Update display */
	dpy_progmode(s, FALSE);
}


/*
 * dbprog_dbclear
 *	Clear in-core CD information.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	reload - Whether we are going to be re-loading the CD information.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_dbclear(curstat_t *s, bool_t reload)
{
	word16_t	flags;
	bool_t		upd_display = FALSE;
	static bool_t	first_time = TRUE;

	/* Pop down the fullname, track details and credits window (if in
	 * track credit mode) if necessary
	 */
	if (XtIsManaged(widgets.fullname.form)) {
		dbprog_fullname_ok(
			widgets.fullname.ok_btn,
			(XtPointer) s,
			(XtPointer) NULL
		);
	}
	if (XtIsManaged(widgets.dbextt.form)) {
		dbprog_extt_ok(
			widgets.dbextt.ok_btn,
			(XtPointer) s,
			(XtPointer) NULL
		);
	}
	if (credits_mode == CREDITS_TRACK) {
		dbprog_credits_ok(
			widgets.credits.ok_btn,
			(XtPointer) s,
			(XtPointer) NULL
		);
	}

	if (s->flags & STAT_SUBMIT) {
		/* Submit in-core information to CDDB, if so designated */
		(void) dbprog_dbsubmit(s);
	}

	if (first_time || s->mode == MOD_NODISC) {
		first_time = FALSE;
		upd_display = TRUE;
	}

	/* Save flags */
	flags = s->flags;

	/* Clear album-specific URLs in the wwwWarp menu */
	if (!wwwwarp_cleared)
		wwwwarp_disc_url_clear(s);

	/* Clear CD information structure */
	cdinfo_clear(reload);

	/* Update CD changer list */
	dbprog_chgr_new(s);

	/* Clear flags */
	s->flags &= ~(STAT_SUBMIT | STAT_EJECT | STAT_EXIT | STAT_CHGDISC);

	/* Set qmode flag */
	s->qmode = QMODE_NONE;

	/* Configure the wwwWarp menu */
	if (!wwwwarp_cleared)
		wwwwarp_sel_cfg(s);

	/* Set the wwwwarp_cleared flag so that if this routine gets
	 * called again before we successfully get CDDB data, we don't
	 * go back into wwwwarp_disc_url_clear() or wwwwarp_sel_cfg()
	 * which has the effect of popping down the wwwwarp menu.
	 */
	wwwwarp_cleared = TRUE;

	if (upd_display) {
		/* Update display */
		dbprog_dpytottime(s);
		dpy_dtitle(s);
		dpy_ttitle(s);

		/* Update curfile */
		dbprog_curfileupd();

		/* Update CD info/program display */
		XmTextSetString(widgets.dbprog.artist_txt, "");
		XmTextSetString(widgets.dbprog.title_txt, "");
		XmListDeleteAllItems(widgets.dbprog.trk_list);
		XmTextSetString(widgets.dbprog.ttitle_txt, "");
		XmTextSetString(widgets.dbprog.pgmseq_txt, "");
		XmTextSetString(widgets.dbextd.notes_txt, "");
		XmTextSetString(widgets.dbextt.notes_txt, "");

		/* Make some buttons insensitive */
		XtSetSensitive(widgets.dbprog.submit_btn, False);
		XtSetSensitive(widgets.dbprog.reload_btn, False);
		XtSetSensitive(widgets.dbprog.fullname_btn, False);
		XtSetSensitive(widgets.dbprog.extd_btn, False);
		XtSetSensitive(widgets.dbprog.dcredits_btn, False);
		XtSetSensitive(widgets.dbprog.segments_btn, False);
		XtSetSensitive(widgets.dbprog.extt_btn, False);
		XtSetSensitive(widgets.dbprog.tcredits_btn, False);

		/* Update button labels */
		dbprog_extt_lblupd(NULL);
		dbprog_tcred_lblupd(NULL);
	}

	/* Clear changed flag */
	dbp->flags &= ~CDINFO_CHANGED;

	/* Eject the CD, if so specified */
	if (flags & STAT_EJECT) {
		cd_busycurs(TRUE, CURS_ALL);

		di_load_eject(s);

		cd_busycurs(FALSE, CURS_ALL);
	}

	/* Change disc, if so specified */
	if (flags & STAT_CHGDISC) {
		if (!s->chgrscan)
			cd_busycurs(TRUE, CURS_ALL);

		di_chgdisc(s);

		/* Update display */
		dpy_dbmode(s, FALSE);
		dpy_playmode(s, FALSE);

		if (!s->chgrscan)
			cd_busycurs(FALSE, CURS_ALL);
	}

	/* Quit the application, if so specified */
	if (flags & STAT_EXIT)
		cd_quit(s);
}


/*
 * dbprog_progget
 *	Get saved track program from file if available.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_progget(curstat_t *s)
{
	cdinfo_ret_t	ret;

	/* Load user-defined track program, if available */
	if ((ret = cdinfo_load_prog(s)) != 0) {
		DBGPRN(DBG_CDI)(errfp, "cdinfo_load_prog: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
	}

	s->program = dbprog_pgm_active();

	/* Update widgets */
	dbprog_structupd(s);

	/* Just loaded from file, so no need to enable the save button
	 * until the program sequence is changed.
	 */
	XtSetSensitive(widgets.dbprog.savepgm_btn, False);

	/* Parse playorder string */
	dbprog_pgm_parse(s);

	/* Update display */
	dpy_progmode(s, FALSE);
}


/*
 * dbprog_dbget
 *	Look up information about the currently loaded disc, if available.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_dbget(curstat_t *s)
{
	cdinfo_ret_t	ret;

	/* Clear the in-core entry */
	dbprog_dbclear(s, TRUE);

	if (s->onetrk_prog) {
		/* Currently playing a single-track program: clear it first */
		s->onetrk_prog = FALSE;
		dbprog_progclear(s);
	}

	/* Set qmode flag */
	s->qmode = QMODE_WAIT;

	/* Update widgets */
	dbprog_structupd(s);
	dpy_dtitle(s);
	dpy_ttitle(s);

	/* Configure the wwwWarp menu */
	wwwwarp_sel_cfg(s);

	/* Update curfile */
	dbprog_curfileupd();

	XtSetSensitive(widgets.dbprog.reload_btn, False);

	/* If in synchronous mode, change to watch cursor, otherwise,
	 * force normal cursor.
	 */
	if (!s->chgrscan)
		cd_busycurs(cdinfo_issync(), CURS_ALL);

	/* Load CD information */
	if ((ret = cdinfo_load(s)) != 0) {
		DBGPRN(DBG_CDI)(errfp, "cdinfo_load: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
	}

	/* Change to normal cursor */
	if (!s->chgrscan && cdinfo_issync())
		cd_busycurs(FALSE, CURS_ALL);

	if (stopload_active) {
		/* Pop down the stop load dialog */
		cd_confirm_popdown();
		stopload_active = FALSE;
	}

	switch (CDINFO_GET_STAT(ret)) {
	case 0:
		/* Success */
		if (dbp->matchlist != NULL ) {
			/* Multiple matches found */
			s->qmode = QMODE_NONE;

			/* Pop up dialog */
			dbprog_matchsel_popup(s);
			return;
		}
		else if ((dbp->flags & CDINFO_NEEDREG) != 0) {
			/* User not registered with CDDB */
			s->qmode = QMODE_NONE;

			/* Pop up CDDB user registration dialog */
			userreg_do_popup(s, TRUE);
		}
		else if ((dbp->flags & CDINFO_MATCH) != 0) {
			/* Exact match found */
			s->qmode = QMODE_MATCH;

			/* Got CDDB data, clear the wwwwarp_cleared flag */
			wwwwarp_cleared = FALSE;
		}
		else
			s->qmode = QMODE_NONE;

		break;

	case AUTH_ERR:
		/* Authorization failed */

		/* Set qmode flag */
		s->qmode = QMODE_NONE;

		if (auth_initted) {
			cd_confirm_popup(
				app_data.str_confirm, app_data.str_authfail,
				dbprog_auth_retry, (XtPointer) 1,
				dbprog_auth_retry, NULL 
			);
		}
		else {
			/* Clear out user name and password */
			XmTextSetString(widgets.auth.name_txt, "");
			XmTextSetString(widgets.auth.pass_txt, "");
			if (dbp->proxy_passwd != NULL) {
				(void) memset(dbp->proxy_passwd, 0,
					      strlen(dbp->proxy_passwd));
				MEM_FREE(dbp->proxy_passwd);
				dbp->proxy_passwd = NULL;
			}

			/* Pop up authorization dialog.
			 * The dialog has mappedWhenManaged set to False,
			 * so we have to map/unmap explicitly.  The reason
			 * for this is we want to avoid a screen glitch when
			 * we move the window in cd_dialog_setpos(), so we
			 * map the window afterwards.
			 */
			if (!XtIsManaged(widgets.auth.form)) {
				XtManageChild(widgets.auth.form);

				/* Set up dialog box position */
				cd_dialog_setpos(XtParent(widgets.auth.form));

				XtMapWidget(XtParent(widgets.auth.form));
			}

			/* Set keyboard focus to the user name field */
			XmProcessTraversal(
				widgets.auth.name_txt,
				XmTRAVERSE_CURRENT
			);
		}
		return;
		/*NOTREACHED*/

	default:
		/* Query error */

		/* Set qmode flag */
		s->qmode = QMODE_ERR;
		break;
	}

	/* Update widgets */
	dbprog_structupd(s);

	/* Configure the wwwWarp menu */
	wwwwarp_sel_cfg(s);

	XtSetSensitive(widgets.dbprog.reload_btn, True);

	/* Update display */
	dpy_dtitle(s);
	dpy_ttitle(s);

	/* Update curfile */
	dbprog_curfileupd();

	/* Add to history and changer lists */
	dbprog_hist_new(s);
	dbprog_chgr_new(s);
}


/*
 * dbprog_chgr_scan_stop
 *	Stop the CD changer scan.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
dbprog_chgr_scan_stop(curstat_t *s)
{
	if (!s->chgrscan)
		return;

	if (scan_id >= 0) {
		cd_untimeout(scan_id);
		scan_id = -1;
	}

	/* Restore original multiplay and reverse modes */
	app_data.multi_play = sav_mplay;
	app_data.reverse = sav_rev;

	s->chgrscan = FALSE;

	if (scan_slot >= 0) {
		/* The user clicked "stop", add the current disc to
		 * the history list.
		 */
		dbprog_hist_new(s);
		scan_slot = -1;
	}
	else if (s->cur_disc != start_slot) {
		/* Go back to original slot */
		if (s->mode != MOD_NODISC && s->mode != MOD_BUSY)
			s->prev_disc = s->cur_disc;
		s->cur_disc = start_slot;
		di_chgdisc(s);

		/* Update display */
		dpy_dbmode(s, FALSE);
		dpy_playmode(s, FALSE);
	}

	start_slot = -1;

	/* Update changer list */
	dbprog_chgr_new(s);

	XtSetSensitive(widgets.dlist.rescan_btn, True);

	/* Pop down the working dialog box */
	cd_working_popdown();

	/* Set to normal cursor */
	cd_busycurs(FALSE, CURS_ALL);
}


/*
 * dbprog_init
 *	Initialize the CD info/program subsystem.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_init(curstat_t *s)
{
	XmString	xs;
	int		i;
	char		*cp;

	/* Check various error message strings to ensure we don't
	 * overflow our error message buffer later.
	 */
	if (((int) (strlen(app_data.str_saverr_fork) + 16) >= ERR_BUF_SZ) ||
	    ((int) (strlen(app_data.str_saverr_suid) + 32) >= ERR_BUF_SZ) ||
	    ((int) (strlen(app_data.str_saverr_open) + 1) >= ERR_BUF_SZ) ||
	    ((int) (strlen(app_data.str_saverr_close) + 1) >= ERR_BUF_SZ) ||
	    ((int) (strlen(app_data.str_saverr_killed) + 16) >= ERR_BUF_SZ) ||
	    ((int) (strlen(app_data.str_saverr_write) + 1) >= ERR_BUF_SZ) ||
	    ((int) (strlen(app_data.str_lnkerr_suid) + 32) >= ERR_BUF_SZ) ||
	    ((int) (strlen(app_data.str_lnkerr_link) + 1) >= ERR_BUF_SZ)) {
		CD_FATAL(app_data.str_longpatherr);
		return;
	}

	time_mode = TIME_TRACK;
	dlist_mode = DLIST_HIST;
	reg_mode = REGION_NONE;
	fname_mode = FNAME_NONE;

	(void) memset(&w_cred, 0, sizeof(cdinfo_credit_t));
	(void) memset(&w_seg, 0, sizeof(cdinfo_segment_t));

	/* Set pointer to CD info struct */
	dbp = dbprog_curdb(s);

	/* Seed some numbers */
	for (i = 0, cp = PROGNAME; i < 4 && *cp != '\0'; i++, cp++)
		s->aux[i] = (byte_t) *cp ^ 0xff;

	/* Clear the in-core structure */
	dbprog_dbclear(s, FALSE);

	/* Save disc details and track details labels,
	 * create versions with asterisk
	 */
	XtVaGetValues(widgets.dbprog.extd_btn,
		XmNlabelString, &xs_extd_lbl,
		NULL
	);
	XtVaGetValues(widgets.dbprog.dcredits_btn,
		XmNlabelString, &xs_dcred_lbl,
		NULL
	);
	XtVaGetValues(widgets.dbprog.segments_btn,
		XmNlabelString, &xs_seg_lbl,
		NULL
	);
	XtVaGetValues(widgets.dbprog.extt_btn,
		XmNlabelString, &xs_extt_lbl,
		NULL
	);
	XtVaGetValues(widgets.dbprog.tcredits_btn,
		XmNlabelString, &xs_tcred_lbl,
		NULL
	);
	XtVaGetValues(widgets.segments.credits_btn,
		XmNlabelString, &xs_scred_lbl,
		NULL
	);
	xs = XmStringCreateSimple("*");
	xs_extd_lblx = XmStringConcat(xs_extd_lbl, xs);
	xs_dcred_lblx = XmStringConcat(xs_dcred_lbl, xs);
	xs_seg_lblx = XmStringConcat(xs_seg_lbl, xs);
	xs_extt_lblx = XmStringConcat(xs_extt_lbl, xs);
	xs_tcred_lblx = XmStringConcat(xs_tcred_lbl, xs);
	xs_scred_lblx = XmStringConcat(xs_scred_lbl, xs);
	XmStringFree(xs);

	/* Initialize the CDDB offline button state */
	XmToggleButtonSetState(
		widgets.dbprog.inetoffln_btn,
		app_data.cdinfo_inetoffln, False
	);

	XtSetSensitive(widgets.dbprog.addpgm_btn, False);
	XtSetSensitive(widgets.dbprog.clrpgm_btn, False);
	XtSetSensitive(widgets.dbprog.savepgm_btn, False);
	XtSetSensitive(widgets.dbprog.submit_btn, False);
	XtSetSensitive(widgets.dbprog.flush_btn,
		       (Boolean) !app_data.cdinfo_inetoffln);
	XtSetSensitive(widgets.dbprog.userreg_btn,
		       (Boolean) (cdinfo_cddb_ver() == 2));

	/* Initialize in-core history and CD changer lists */
	cdinfo_hist_init();
	cdinfo_chgr_init();

	XtVaSetValues(widgets.dlist.type_opt,
		XmNmenuHistory, widgets.dlist.hist_btn,
		NULL
	);

	/* Set disc details and track details window genre selectors */
	for (i = 0; i < 2; i++) {
		XtVaSetValues(widgets.dbextd.genre_opt[i],
			XmNmenuHistory, widgets.dbextd.genre_none_btn[i],
			NULL
		);
		XtVaSetValues(widgets.dbextd.subgenre_opt[i],
			XmNmenuHistory, widgets.dbextd.subgenre_none_btn[i],
			NULL
		);
		XtVaSetValues(widgets.dbextt.genre_opt[i],
			XmNmenuHistory, widgets.dbextt.genre_none_btn[i],
			NULL
		);
		XtVaSetValues(widgets.dbextt.subgenre_opt[i],
			XmNmenuHistory, widgets.dbextt.subgenre_none_btn[i],
			NULL
		);
	}

	/* Set credits window role selectors */
	XtVaSetValues(widgets.credits.prirole_opt,
		XmNmenuHistory, widgets.credits.prirole_none_btn,
		NULL
	);
	XtVaSetValues(widgets.credits.subrole_opt,
		XmNmenuHistory, widgets.credits.subrole_none_btn,
		NULL
	);
}


/*
 * dbprog_chgsubmit
 *	If in-core info has been changed, ask user if it should
 *	be submitted to CDDB.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Whether the application should proceed to shut down
 */
/*ARGSUSED*/
bool_t
dbprog_chgsubmit(curstat_t *s)
{
	if ((dbp->flags & CDINFO_CHANGED) && !app_data.cdinfo_inetoffln) {
		/* We use cd_timeout to schedule a pop-up of the
		 * confirm dialog, because this thread could be
		 * called from a cd_confirm_dialog button callback,
		 * and we cd_confirm_callback is not re-entrant in
		 * this manner
		 */
		(void) cd_timeout(50, dbprog_submit_popup, NULL);
		return FALSE;
	}

	return TRUE;
}


/*
 * dbprog_curartist
 *	Return the current disc artist string.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Disc title text string, or the null string if there
 *	is no title available.
 */
char *
dbprog_curartist(curstat_t *s)
{
	if (s->mode == MOD_BUSY || s->mode == MOD_NODISC ||
	    s->qmode == QMODE_WAIT)
		return NULL;

	return (dbp->disc.artist);
}


/*
 * dbprog_curtitle
 *	Return the current disc title string.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Disc title text string, or the null string if there
 *	is no title available.
 */
char *
dbprog_curtitle(curstat_t *s)
{
	if (s->mode == MOD_BUSY || s->mode == MOD_NODISC ||
	    s->qmode == QMODE_WAIT)
		return NULL;

	return (dbp->disc.title == NULL ?
		app_data.str_unkndisc : dbp->disc.title);
}


/*
 * dbprog_curttitle
 *	Return the current track title string.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Track title text string, or the null string if there
 *	is no title available.
 */
char *
dbprog_curttitle(curstat_t *s)
{
	int	n = di_curtrk_pos(s);

	if (s->mode == MOD_BUSY || s->mode == MOD_NODISC ||
	    (int) s->cur_trk < 0 || s->qmode == QMODE_WAIT)
		return ("");

	if (n < 0 || dbp->track[n].title == NULL)
		return (app_data.str_unkntrk);

	return (dbp->track[n].title);
}


/*
 * dbprog_curdb
 *	Obtain the cdinfo_incore_t structure for the current disc.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Pointer to the cdinfo_incore_t structure.
 */
/*ARGSUSED*/
cdinfo_incore_t *
dbprog_curdb(curstat_t *s)
{
	return (cdinfo_addr());
}


/*
 * dbprog_curseltrk
 *	Obtain the currently selected track number in the track list
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	The selected track number, or -1 if not selected.
 */
/*ARGSUSED*/
int
dbprog_curseltrk(curstat_t *s)
{
	return (sel_pos);
}


/*
 * dbprog_pgm_parse
 *	Parse the program mode play sequence text string, and
 *	update the playorder table in the curstat_t structure.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	TRUE=success, FALSE=error.
 */
bool_t
dbprog_pgm_parse(curstat_t *s)
{
	int	i,
		j;
	char	*p,
		*q,
		*tmpbuf;
	bool_t	last = FALSE,
		skipped = FALSE;

	if (dbp->playorder == NULL)
		/* Nothing to do */
		return TRUE;

	tmpbuf = NULL;
	if (!util_newstr(&tmpbuf, dbp->playorder)) {
		CD_FATAL(app_data.str_nomemory);
		return FALSE;
	}

	s->prog_tot = 0;

	for (i = 0, p = q = tmpbuf; i < MAXTRACK; p = ++q) {
		/* Skip p to the next digit */
		for (; !isdigit((int) *p) && *p != '\0'; p++)
			;

		if (*p == '\0')
			/* No more to do */
			break;

		/* Skip q to the next non-digit */
		for (q = p; isdigit((int) *q); q++)
			;

		if (*q == ',')
			*q = '\0';
		else if (*q == '\0')
			last = TRUE;
		else {
			MEM_FREE(tmpbuf);
			CD_WARNING(app_data.str_seqfmterr);
			return FALSE;
		}

		if (q > p) {
			/* Update play sequence */
			for (j = 0; j < MAXTRACK; j++) {
				if (s->trkinfo[j].trkno == atoi(p)) {
					s->playorder[i] = j;
					s->prog_tot++;
					i++;
					break;
				}
			}

			if (j >= MAXTRACK)
				skipped = TRUE;
		}

		if (last)
			break;
	}

	if (skipped) {
		/* Delete invalid tracks from list */

		tmpbuf[0] = '\0';
		for (i = 0; i < (int) s->prog_tot; i++) {
			if (i == 0)
				(void) sprintf(tmpbuf, "%u",
					    s->trkinfo[s->playorder[i]].trkno);
			else
				(void) sprintf(tmpbuf, "%s,%u",
					    tmpbuf,
					    s->trkinfo[s->playorder[i]].trkno);
		}

		XmTextSetString(widgets.dbprog.pgmseq_txt, tmpbuf);

		CD_WARNING(app_data.str_invpgmtrk);
	}

	MEM_FREE(tmpbuf);

	return TRUE;
}


/*
 * dbprog_segments_setmode
 *	Set the "set" button sensitivity and pointer label configuration
 *	in the segments window.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_segments_setmode(curstat_t *s)
{
	int			segptr_mode = 0;
	static XmString		xs_set,
				xs_clear;
	XmListCallbackStruct	cb;
	static int		segptr_prev = -1;
	static bool_t		first = TRUE;

	if (!XtIsManaged(widgets.segments.form))
		return;

	if (first) {
		first = FALSE;
		XtVaGetValues(widgets.segments.set_btn,
			XmNlabelString, &xs_set,
			NULL
		);
		XtVaGetValues(widgets.dbprog.clrpgm_btn,
			XmNlabelString, &xs_clear,
			NULL
		);
	}

	switch (s->mode) {
	case MOD_PLAY:
	case MOD_PAUSE:
	case MOD_STOP:
		if (s->program || s->shuffle)
			segptr_mode = 0;
		else if (s->segplay == SEGP_AB)
			segptr_mode = 3;
		else if (s->mode != MOD_STOP) {
			if (s->segplay == SEGP_NONE)
				segptr_mode = 1;
			else if (s->segplay == SEGP_A)
				segptr_mode = 2;
		}
		else
			segptr_mode = 0;
		break;
	case MOD_SAMPLE:
	default:
		segptr_mode = 0;
		break;
	}

	if (segptr_prev == segptr_mode)
		/* No change */
		return;

	XtVaSetValues(widgets.segments.set_btn,
		XmNlabelString, (segptr_mode == 3) ? xs_clear : xs_set,
		NULL
	);
			
	XtSetSensitive(widgets.segments.set_btn, (Boolean) (segptr_mode > 0));

	switch (segptr_mode) {
	case 1:
		XtMapWidget(widgets.segments.startptr_lbl);
		XtUnmapWidget(widgets.segments.endptr_lbl);
		break;
	case 2:
		XtUnmapWidget(widgets.segments.startptr_lbl);
		XtMapWidget(widgets.segments.endptr_lbl);
		break;
	case 3:
		XtMapWidget(widgets.segments.startptr_lbl);
		XtMapWidget(widgets.segments.endptr_lbl);
		break;
	case 0:
	default:
		XtUnmapWidget(widgets.segments.startptr_lbl);
		XtUnmapWidget(widgets.segments.endptr_lbl);

		/* Put focus on the OK button */
		XmProcessTraversal(widgets.segments.ok_btn,
				   XmTRAVERSE_CURRENT);

		if (seg_pos > 0) {
			/* If dbprog_segments_select is ever changed to use
			 * more fields of the callback struct, this will need
			 * change too.
			 */
			cb.item_position = seg_pos;

			/* Fake a callback */
			dbprog_segments_select(
				widgets.segments.seg_list,
				(XtPointer) s,
				(XtPointer) &cb
			);
		}
		break;
	}

	segptr_prev = segptr_mode;
}


/*
 * dbprog_segments_cancel
 *	Cancel a->b mode in the segments window
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
dbprog_segments_cancel(curstat_t *s)
{
	XmListCallbackStruct	cb;

	if (!XtIsManaged(widgets.segments.form))
		/* Nothing to do */
		return;

	if (seg_pos < 0) {
		/* Simply set some fields to blank */
		XmTextSetString(widgets.segments.starttrk_txt, "");
		XmTextSetString(widgets.segments.startfrm_txt, "");
		XmTextSetString(widgets.segments.endtrk_txt, "");
		XmTextSetString(widgets.segments.endfrm_txt, "");
		return;
	}

	/* If dbprog_segments_select is ever changed to use more
	 * fields of the callback struct, this will need change too.
	 */
	cb.item_position = seg_pos;

	/* Fake a callback */
	dbprog_segments_select(
		widgets.segments.seg_list,
		(XtPointer) s,
		(XtPointer) &cb
	);
}


/*
 * dbprog_stopload_active
 *	Query and/or set the stopload_active boolean flag.
 *
 * Args:
 *	mode - 0 for query only, none-zero to set the stopload_active value
 *	newval - If mode is non-zero, the value to set stopload_active to.
 *
 * Return:
 *	If mode is 0, the current stopload_active value.
 *	If mode is non-zero, the original stopload_active value.
 */
bool_t
dbprog_stopload_active(int mode, bool_t newval)
{
	bool_t val;

	val = stopload_active;
	if (mode != 0)
		stopload_active = newval;

	return (val);
}


/**************** vv Callback routines vv ****************/

/*
 * dbprog_popup
 *	Pop up the CD info/program subsystem window.
 */
/*ARGSUSED*/
void
dbprog_popup(Widget w, XtPointer client_data, XtPointer call_data)
{
	static bool_t	first = TRUE;

	if (XtIsManaged(widgets.dbprog.form)) {
		/* Already popped up: pop it down */
		dbprog_ok(widgets.dbprog.ok_btn, client_data, call_data);
		return;
	}

	/* Pop up the dbprog window.
	 * The dialog has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason
	 * for this is we want to avoid a screen glitch when
	 * we move the window in cd_dialog_setpos(), so we
	 * map the window afterwards.
	 */
	XtManageChild(widgets.dbprog.form);
	if (first) {
		first = FALSE;
		/* Set window position */
		cd_dialog_setpos(XtParent(widgets.dbprog.form));
	}
	XtMapWidget(XtParent(widgets.dbprog.form));

	/* Put focus on the OK button */
	XmProcessTraversal(widgets.dbprog.ok_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_inetoffln
 *	The Internet Offline button callback.
 */
/*ARGSUSED*/
void
dbprog_inetoffln(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmToggleButtonCallbackStruct	*p =
		(XmToggleButtonCallbackStruct *)(void *) call_data;
	curstat_t			*s = (curstat_t *)(void *) client_data;
	cdinfo_ret_t			ret;

	if (p->reason != XmCR_VALUE_CHANGED)
		return;

	DBGPRN(DBG_CDI)(errfp,
		"\n* OFFLINE-CDDB: %s\n", p->set ? "On" : "Off");

	/* If CD information lookup is in progress, cancel it, then
	 * schedule a reload in one second.
	 */
	if (s->qmode == QMODE_WAIT) {
		cdinfo_load_cancel();
		(void) cd_timeout(1000, dbprog_dbget, (byte_t *) s);
	}

	app_data.cdinfo_inetoffln = (bool_t) p->set;

	if ((ret = cdinfo_offline(s)) != 0) {
		DBGPRN(DBG_CDI)(errfp,
			"cdinfo_offline: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));

		cd_beep();
		XmToggleButtonSetState(w, !p->set, False);
		app_data.cdinfo_inetoffln = (bool_t) !p->set;
		return;
	}

	if (!app_data.cdinfo_inetoffln && (dbp->flags & CDINFO_CHANGED))
		XtSetSensitive(widgets.dbprog.submit_btn, True);
	else
		XtSetSensitive(widgets.dbprog.submit_btn, False);

	XtSetSensitive(widgets.dbprog.flush_btn, !app_data.cdinfo_inetoffln);

	/* Configure the wwwWarp menu */
	wwwwarp_sel_cfg(s);
}


/*
 * dbprog_txtline_verify
 *	Single-line text widget user-input verification callback.
 */
/*ARGSUSED*/
void
dbprog_txtline_verify(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmTextVerifyCallbackStruct
		*p = (XmTextVerifyCallbackStruct *)(void *) call_data;
	int	i;

	if (p->reason != XmCR_MODIFYING_TEXT_VALUE)
		return;

	p->doit = True;

	if (p->startPos != p->endPos)
		/* Deleting text, no verification needed */
		return;

	switch (p->text->format) {
	case XmFMT_8_BIT:
		for (i = 0; i < p->text->length; i++) {
			/* This is a single-line text widget, so a
			 * newline is not allowed.
			 */
			if (p->text->ptr[i] == '\n' ||
			    p->text->ptr[i] == '\r') {
				p->doit = False;
				break;
			}
		}
		break;

	case XmFMT_16_BIT:
	default:
		/* Nothing to do here */
		break;
	}
}


/*
 * dbprog_txtnline_verify
 *	Single-line text widget numerical user-input verification callback.
 */
/*ARGSUSED*/
void
dbprog_txtnline_verify(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmTextVerifyCallbackStruct
		*p = (XmTextVerifyCallbackStruct *)(void *) call_data;
	int	i;

	if (p->reason != XmCR_MODIFYING_TEXT_VALUE)
		return;

	p->doit = True;

	if (p->startPos != p->endPos)
		/* Deleting text, no verification needed */
		return;

	switch (p->text->format) {
	case XmFMT_8_BIT:
		for (i = 0; i < p->text->length; i++) {
			/* Only digits are allowed */
			if (!isdigit((int) p->text->ptr[i]) &&
			    p->text->ptr[i] != '+' &&
			    p->text->ptr[i] != '-') {
				p->doit = False;
				break;
			}
		}
		break;

	case XmFMT_16_BIT:
	default:
		/* Nothing to do here */
		break;
	}
}


/*
 * dbprog_text_new
 *	Generic text widget callback function.
 */
/*ARGSUSED*/
void
dbprog_text_new(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmAnyCallbackStruct	*p = (XmAnyCallbackStruct *)(void *) call_data;
	curstat_t		*s = (curstat_t *)(void *) client_data;
	char			*str,
				**ent;
	bool_t			credits_handling,
				segments_handling,
				fname_handling;
	cdinfo_fname_t		*fnp = NULL;

	credits_handling = segments_handling = fname_handling = FALSE;

	/* Figure out which widget changed */
	if (w == widgets.dbprog.artist_txt)
		ent = &dbp->disc.artist;
	else if (w == widgets.dbprog.title_txt)
		ent = &dbp->disc.title;
	else if (w == widgets.dbextd.sorttitle_txt)
		ent = &dbp->disc.sorttitle;
	else if (w == widgets.dbextd.the_txt)
		ent = &dbp->disc.title_the;
	else if (w == widgets.dbextd.year_txt)
		ent = &dbp->disc.year;
	else if (w == widgets.dbextd.label_txt)
		ent = &dbp->disc.label;
	else if (w == widgets.dbextd.dnum_txt)
		ent = &dbp->disc.dnum;
	else if (w == widgets.dbextd.tnum_txt)
		ent = &dbp->disc.tnum;
	else if (w == widgets.dbextd.notes_txt)
		ent = &dbp->disc.notes;
	else if (w == widgets.dbextt.sorttitle_txt)
		ent = &dbp->track[extt_pos].sorttitle;
	else if (w == widgets.dbextt.the_txt)
		ent = &dbp->track[extt_pos].title_the;
	else if (w == widgets.dbextt.artist_txt)
		ent = &dbp->track[extt_pos].artist;
	else if (w == widgets.dbextt.year_txt)
		ent = &dbp->track[extt_pos].year;
	else if (w == widgets.dbextt.label_txt)
		ent = &dbp->track[extt_pos].label;
	else if (w == widgets.dbextt.bpm_txt)
		ent = &dbp->track[extt_pos].bpm;
	else if (w == widgets.dbextt.notes_txt)
		ent = &dbp->track[extt_pos].notes;
	else if (w == widgets.credits.name_txt) {
		ent = &w_cred.crinfo.name;
		credits_handling = TRUE;
	}
	else if (w == widgets.credits.notes_txt) {
		ent = &w_cred.notes;
		credits_handling = TRUE;
	}
	else if (w == widgets.segments.name_txt) {
		ent = &w_seg.name;
		segments_handling = TRUE;
	}
	else if (w == widgets.segments.starttrk_txt) {
		ent = &w_seg.start_track;
		segments_handling = TRUE;
	}
	else if (w == widgets.segments.startfrm_txt) {
		ent = &w_seg.start_frame;
		segments_handling = TRUE;
	}
	else if (w == widgets.segments.endtrk_txt) {
		ent = &w_seg.end_track;
		segments_handling = TRUE;
	}
	else if (w == widgets.segments.endfrm_txt) {
		ent = &w_seg.end_frame;
		segments_handling = TRUE;
	}
	else if (w == widgets.segments.notes_txt) {
		ent = &w_seg.notes;
		segments_handling = TRUE;
	}
	else if (w == widgets.fullname.dispname_txt) {
		switch (fname_mode) {
		case FNAME_DISC:
			fnp = &dbp->disc.artistfname;
			break;
		case FNAME_TRACK:
			fnp = &dbp->track[extt_pos].artistfname;
			break;
		case FNAME_CREDITS:
			fnp = &w_cred.crinfo.fullname;
			credits_handling = TRUE;
			break;
		default:
			/* Unsupported fname mode */
			return;
		}
		ent = &fnp->dispname;
		fname_handling = TRUE;
	}
	else if (w == widgets.fullname.lastname_txt) {
		switch (fname_mode) {
		case FNAME_DISC:
			fnp = &dbp->disc.artistfname;
			break;
		case FNAME_TRACK:
			fnp = &dbp->track[extt_pos].artistfname;
			break;
		case FNAME_CREDITS:
			fnp = &w_cred.crinfo.fullname;
			credits_handling = TRUE;
			break;
		default:
			/* Unsupported fname mode */
			return;
		}
		ent = &fnp->lastname;
		fname_handling = TRUE;
	}
	else if (w == widgets.fullname.firstname_txt) {
		switch (fname_mode) {
		case FNAME_DISC:
			fnp = &dbp->disc.artistfname;
			break;
		case FNAME_TRACK:
			fnp = &dbp->track[extt_pos].artistfname;
			break;
		case FNAME_CREDITS:
			fnp = &w_cred.crinfo.fullname;
			credits_handling = TRUE;
			break;
		default:
			/* Unsupported fname mode */
			return;
		}
		ent = &fnp->firstname;
		fname_handling = TRUE;
	}
	else if (w == widgets.fullname.the_txt) {
		switch (fname_mode) {
		case FNAME_DISC:
			fnp = &dbp->disc.artistfname;
			break;
		case FNAME_TRACK:
			fnp = &dbp->track[extt_pos].artistfname;
			break;
		case FNAME_CREDITS:
			fnp = &w_cred.crinfo.fullname;
			credits_handling = TRUE;
			break;
		default:
			/* Unsupported fname mode */
			return;
		}
		ent = &fnp->the;
		fname_handling = TRUE;
	}
	else {
		/* Unsupported by this function */
		return;
	}

	/* Update the appropriate in-core structure entry */
	switch (p->reason) {
	case XmCR_VALUE_CHANGED:
		if ((str = XmTextGetString(w)) == NULL)
			return;

		if (*ent != NULL) {
			if (strcmp(str, *ent) == 0) {
				/* not changed */
				XtFree(str);
				break;
			}
			MEM_FREE(*ent);
			*ent = NULL;
		}

		if (!util_newstr(ent, str)) {
			CD_FATAL(app_data.str_nomemory);
			XtFree(str);
			return;
		}

		if (fname_handling) {
			fname_changed = TRUE;

			if (w != widgets.fullname.dispname_txt &&
			    XmToggleButtonGetState(
				    widgets.fullname.autogen_btn)) {
				char	*cp;

				/* Auto-generate the fullname's dispname */
				cp = dbprog_autoname(fnp);
				XmTextSetString(
					widgets.fullname.dispname_txt,
					cp
				);
				MEM_FREE(cp);
			}
		}

		/* Special handling for fullname.dispname field:
		 * update the associated fields for album, track,
		 * or credit to match.
		 */
		if (w == widgets.fullname.dispname_txt) {
			switch (fname_mode) {
			case FNAME_DISC:
				XmTextSetString(
					widgets.dbprog.artist_txt, str
				);
				break;
			case FNAME_TRACK:
				XmTextSetString(
					widgets.dbextt.artist_txt, str
				);
				break;
			case FNAME_CREDITS:
				XmTextSetString(
					widgets.credits.name_txt, str
				);
				break;
			}
		}

		/* Update disc details button label if needed */
		if (w == widgets.dbextd.notes_txt)
			dbprog_extd_lblupd();

		/* Update credits window title if needed */
		if (w == widgets.segments.name_txt)
			dbprog_set_seg_title();

		/* Update the disc details window and credits window titles
		 * if needed
		 */
		if (w == widgets.dbprog.artist_txt ||
		    w == widgets.dbprog.title_txt) {
			if (XtIsManaged(widgets.dbextd.form)) {
				/* Update disc details disc title */
				dbprog_set_disc_title(
					s->cur_disc,
					dbp->disc.artist,
					dbp->disc.title
				);
			}

			/* Update main window title display */
			dpy_dtitle(s);
		}

		XtFree(str);

		if (credits_handling) {
			if (cred_pos > 0)
				XtSetSensitive(widgets.credits.mod_btn, True);
			else
				XtSetSensitive(widgets.credits.add_btn, True);
		}
		else if (segments_handling) {
			if (seg_pos > 0)
				XtSetSensitive(widgets.segments.mod_btn, True);
			else
				XtSetSensitive(widgets.segments.add_btn, True);
		}
		else {
			dbp->flags |= CDINFO_CHANGED;
			if (!app_data.cdinfo_inetoffln) {
				XtSetSensitive(widgets.dbprog.submit_btn,
					       True);
			}
		}

		break;

	case XmCR_ACTIVATE:
	case XmCR_LOSING_FOCUS:
		if (w == widgets.dbprog.title_txt ||
		    w == widgets.dbextd.sorttitle_txt ||
		    w == widgets.dbextd.year_txt ||
		    w == widgets.dbextd.label_txt ||
		    w == widgets.dbextd.dnum_txt ||
		    w == widgets.dbextd.tnum_txt ||
		    w == widgets.dbextt.sorttitle_txt ||
		    w == widgets.dbextt.artist_txt ||
		    w == widgets.dbextt.year_txt ||
		    w == widgets.dbextt.label_txt ||
		    w == widgets.dbextt.bpm_txt ||
		    w == widgets.fullname.dispname_txt ||
		    w == widgets.fullname.lastname_txt ||
		    w == widgets.fullname.firstname_txt) {

			/* Set cursor to beginning of text */
			XmTextSetInsertionPosition(w, 0);

			if (w == widgets.dbprog.title_txt) {
				/* Update curfile */
				dbprog_curfileupd();
			}
		}
		break;

	default:
		break;
	}
}


/*
 * dbprog_focus_next
 *	Change focus to the next widget on done
 */
/*ARGSUSED*/
void
dbprog_focus_next(Widget w, XtPointer client_data, XtPointer call_data)
{
	Widget	nextw;

	if (w == widgets.dbprog.artist_txt)
		nextw = widgets.dbprog.title_txt;
	else if (w == widgets.dbprog.title_txt)
		nextw = widgets.dbprog.ttitle_txt;
	else if (w == widgets.dbextd.sorttitle_txt)
		nextw = widgets.dbextd.the_btn;
	else if (w == widgets.dbextd.the_txt)
		nextw = widgets.dbextd.year_txt;
	else if (w == widgets.dbextd.year_txt)
		nextw = widgets.dbextd.label_txt;
	else if (w == widgets.dbextd.label_txt)
		nextw = widgets.dbextd.comp_btn;
	else if (w == widgets.dbextd.dnum_txt)
		nextw = widgets.dbextd.tnum_txt;
	else if (w == widgets.dbextd.tnum_txt)
		nextw = widgets.dbextd.region_chg_btn;
	else if (w == widgets.dbextt.sorttitle_txt)
		nextw = widgets.dbextt.the_btn;
	else if (w == widgets.dbextt.the_txt)
		nextw = widgets.dbextt.artist_txt;
	else if (w == widgets.dbextt.artist_txt)
		nextw = widgets.dbextt.fullname_btn;
	else if (w == widgets.dbextt.year_txt)
		nextw = widgets.dbextt.label_txt;
	else if (w == widgets.dbextt.label_txt)
		nextw = widgets.dbextt.bpm_txt;
	else if (w == widgets.credits.name_txt)
		nextw = widgets.credits.fullname_btn;
	else if (w == widgets.segments.name_txt)
		nextw = widgets.segments.starttrk_txt;
	else if (w == widgets.segments.starttrk_txt)
		nextw = widgets.segments.startfrm_txt;
	else if (w == widgets.segments.startfrm_txt)
		nextw = widgets.segments.endtrk_txt;
	else if (w == widgets.segments.endtrk_txt)
		nextw = widgets.segments.endfrm_txt;
	else if (w == widgets.segments.endfrm_txt)
		nextw = widgets.segments.notes_txt;
	else if (w == widgets.fullname.dispname_txt)
		nextw = widgets.fullname.firstname_txt;
	else if (w == widgets.fullname.firstname_txt)
		nextw = widgets.fullname.lastname_txt;
	else if (w == widgets.fullname.lastname_txt)
		nextw = widgets.fullname.the_btn;
	else if (w == widgets.fullname.the_txt)
		nextw = widgets.fullname.ok_btn;
	else if (w == widgets.auth.name_txt)
		nextw = widgets.auth.pass_txt;
	else if (w == widgets.auth.pass_txt)
		nextw = widgets.auth.ok_btn;
	else
		return;

	/* Put the input focus on the next widget */
	XmProcessTraversal(nextw, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_trklist_play
 *	Track list entry selection default action callback.
 */
void
dbprog_trklist_play(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmListCallbackStruct	*p = (XmListCallbackStruct *)(void *) call_data;
	curstat_t		*s = (curstat_t *)(void *) client_data;

	if (p->reason != XmCR_DEFAULT_ACTION)
		return;

	/* Stop current playback */
	if (s->mode != MOD_STOP)
		di_stop(s, FALSE);

	sel_pos = p->item_position;

	/* Clear previous program */
	XmTextSetString(widgets.dbprog.pgmseq_txt, "");
	XtSetSensitive(widgets.dbprog.addpgm_btn, False);
	XtSetSensitive(widgets.dbprog.clrpgm_btn, False);
	XtSetSensitive(widgets.dbprog.savepgm_btn, False);
	dbprog_progclear(s);

	/* This is a single-track program as a result of double clicking
	 * on a track (or pressing return).
	 */
	s->onetrk_prog = TRUE;

	/* Add selected track to program */
	dbprog_addpgm(w, client_data, call_data);

	/* Play selected track */
	cd_play_pause(w, client_data, call_data);

	if (sel_pos > 0) {
		XmListDeselectPos(w, sel_pos);
		sel_pos = -1;
	}
	XmTextSetString(widgets.dbprog.ttitle_txt, "");

	XtSetSensitive(widgets.dbprog.addpgm_btn, False);
	XtSetSensitive(widgets.dbprog.extt_btn, False);
	XtSetSensitive(widgets.dbprog.tcredits_btn, False);

	/* Update button labels */
	dbprog_extt_lblupd(NULL);
	dbprog_tcred_lblupd(NULL);
}


/*
 * dbprog_trklist_select
 *	Track list entry selection callback.
 */
void
dbprog_trklist_select(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmListCallbackStruct	*p = (XmListCallbackStruct *)(void *) call_data;
	curstat_t		*s = (curstat_t *)(void *) client_data;
	int			pos;
	char			*cp;
	bool_t			resel_list;

	if (p->reason != XmCR_BROWSE_SELECT)
		return;

	if (!di_check_disc(s) || s->mode == MOD_BUSY || s->mode == MOD_NODISC)
		return;

	pos = p->item_position - 1;

	if (title_edited) {
		title_edited = FALSE;

		if ((cp = XmTextGetString(widgets.dbprog.ttitle_txt)) == NULL)
			return;

		/* Update track list entry */
		dbprog_listupd_ent(s, pos, cp, FALSE);

		if (sel_pos > 0) {
			XmListDeselectPos(w, sel_pos);
			sel_pos = -1;
		}
		XmTextSetString(widgets.dbprog.ttitle_txt, "");

		if (!util_newstr(&dbp->track[pos].title, cp)) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}

		XtFree(cp);

		dbp->flags |= CDINFO_CHANGED;
		if (!app_data.cdinfo_inetoffln)
			XtSetSensitive(widgets.dbprog.submit_btn, True);
		XtSetSensitive(widgets.dbprog.addpgm_btn, False);
		XtSetSensitive(widgets.dbprog.extt_btn, False);
		XtSetSensitive(widgets.dbprog.tcredits_btn, False);

		/* Update button labels */
		dbprog_extt_lblupd(NULL);
		dbprog_tcred_lblupd(NULL);

		/* Update the track details window if necessary */
		if (extt_pos == pos &&
		    (XtIsManaged(widgets.dbextt.form) ||
		     XtIsManaged(widgets.credits.form))) {
			dbprog_set_track_title(
				s->trkinfo[pos].trkno,
				dbp->track[pos].title
			);
		}

		/* Update the main window if necessary */
		if (di_curtrk_pos(s) == pos) {
			dpy_ttitle(s);

			/* Update curfile */
			dbprog_curfileupd();
		}

		/* Return the input focus to the track title editor */
		XmProcessTraversal(
			widgets.dbprog.ttitle_txt,
			XmTRAVERSE_CURRENT
		);
	}
	else if (sel_pos == p->item_position) {
		/* This item is already selected: deselect it */

		XmListDeselectPos(w, sel_pos);
		sel_pos = ind_pos = -1;
		XmTextSetString(widgets.dbprog.ttitle_txt, "");

		XtSetSensitive(widgets.dbprog.addpgm_btn, False);
		XtSetSensitive(widgets.dbprog.extt_btn, False);
		XtSetSensitive(widgets.dbprog.tcredits_btn, False);

		/* Update button labels */
		dbprog_extt_lblupd(NULL);
		dbprog_tcred_lblupd(NULL);
	}
	else {
		/* Check full name data and abort if needed */
		if (!dbprog_fullname_ck(s)) {
			cd_beep();
			XmListDeselectPos(w, p->item_position);
			XmListSelectPos(w, sel_pos, False);
			return;
		}

		sel_pos = p->item_position;

		if (dbp->track[pos].title == NULL) {
			XmTextSetString(widgets.dbprog.ttitle_txt, "");
		}
		else {
			XmTextSetString(widgets.dbprog.ttitle_txt,
					dbp->track[pos].title);
			XmTextSetInsertionPosition(
				widgets.dbprog.ttitle_txt,
				strlen(dbp->track[pos].title)
			);
		}

		XtSetSensitive(widgets.dbprog.addpgm_btn, True);
		XtSetSensitive(widgets.dbprog.extt_btn, True);
		XtSetSensitive(widgets.dbprog.tcredits_btn, True);

		/* Update button labels */
		dbprog_extt_lblupd(&dbp->track[pos]);
		dbprog_tcred_lblupd(&dbp->track[pos]);

		resel_list = FALSE;

		/* Warp the track details window to the new selected track,
		 * if it is popped up.
		 */
		if (XtIsManaged(widgets.dbextt.form)) {
			extt_pos = pos;
			dbprog_extt(w, (XtPointer) FALSE, call_data);
			resel_list = TRUE;
		}

		/* Warp the credits window to the new selected track,
		 * if it is popped up and in track mode.
		 */
		if (credits_mode == CREDITS_TRACK) {
			/* Update credits window if needed */
			extt_pos = pos;
			dbprog_creditupd(s, pos);
			resel_list = TRUE;
		}

		if (resel_list)
			XmListSelectPos(w, sel_pos, False);

		/* Update fullname window if necessary */
		if (fname_mode == FNAME_TRACK) {
			dbprog_fullname(widgets.dbextt.fullname_btn,
					(XtPointer) FALSE, call_data);
		}
		else if (fname_mode == FNAME_CREDITS &&
			 credits_mode == CREDITS_TRACK) {
			dbprog_fullname(widgets.credits.fullname_btn,
					(XtPointer) FALSE, call_data);
		}
	}
}


/*
 * dbprog_ttitle_focuschg
 *	Track title editor text widget keyboard focus change callback.
 */
/*ARGSUSED*/
void
dbprog_ttitle_focuschg(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmAnyCallbackStruct	*p = (XmAnyCallbackStruct *)(void *) call_data;
	curstat_t		*s = (curstat_t *)(void *) client_data;
	int			i;

	switch (p->reason) {
	case XmCR_FOCUS:
		if (sel_pos < 0) {
			for (i = 0; i < (int) s->tot_trks; i++) {
				if (dbp->track[i].title == NULL) {
					ind_pos = i + 1;

					XmListSelectPos(
						widgets.dbprog.trk_list,
						ind_pos,
						False
					);
					XmListSetBottomPos(
						widgets.dbprog.trk_list,
						ind_pos
					);
					break;
				}
			}
		}
		break;

	case XmCR_LOSING_FOCUS:
		if (ind_pos > 0) {
			XmListDeselectPos(widgets.dbprog.trk_list, ind_pos);
			ind_pos = -1;
		}
		break;

	default:
		break;
	}
}


/*
 * dbprog_ttitle_new
 *	Track title editor text widget callback function.
 */
void
dbprog_ttitle_new(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmAnyCallbackStruct	*p = (XmAnyCallbackStruct *)(void *) call_data;
	curstat_t		*s = (curstat_t *)(void *) client_data;
	char			*cp;
	int			*pos,
				i;
	XmListCallbackStruct	cb;

	if (p->reason == XmCR_VALUE_CHANGED) {
		if ((cp = XmTextGetString(w)) == NULL)
			return;

		if (*cp == '\0')
			title_edited = FALSE;
		else if (sel_pos < 0)
			title_edited = TRUE;

		XtFree(cp);
		return;
	}
	else if (p->reason != XmCR_ACTIVATE)
		return;

	if (sel_pos > 0 &&
	    XmListGetSelectedPos(widgets.dbprog.trk_list, &pos, &i)) {
		if ((cp = XmTextGetString(w)) == NULL)
			return;

		if (pos == NULL) {
			XtFree(cp);
			return;
		}

		if (i != 1)
			XtFree(cp);
		else {
			/* Update track list entry */
			dbprog_listupd_ent(s, (*pos)-1, cp, FALSE);

			XmListDeselectPos(widgets.dbprog.trk_list, sel_pos);
			sel_pos = -1;

			if (!util_newstr(&dbp->track[(*pos)-1].title, cp)) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}

			XtFree(cp);

			dbp->flags |= CDINFO_CHANGED;
			if (!app_data.cdinfo_inetoffln)
			    XtSetSensitive(widgets.dbprog.submit_btn, True);
			XtSetSensitive(widgets.dbprog.addpgm_btn, False);
			XtSetSensitive(widgets.dbprog.extt_btn, False);
			XtSetSensitive(widgets.dbprog.tcredits_btn, False);

			/* Update button labels */
			dbprog_extt_lblupd(NULL);
			dbprog_tcred_lblupd(NULL);

			/* Update the track details and credits windows if
			 * necessary
			 */
			if (extt_pos == (*pos)-1 &&
			    (XtIsManaged(widgets.dbextt.form) ||
			     XtIsManaged(widgets.credits.form))) {
				dbprog_set_track_title(
					s->trkinfo[extt_pos].trkno,
					dbp->track[extt_pos].title
				);
			}

			/* Update the main window if necessary */
			if (di_curtrk_pos(s) == (*pos)-1) {
				dpy_ttitle(s);

				/* Update curfile */
				dbprog_curfileupd();
			}
		}

		XmTextSetString(w, "");

		XtFree((XtPointer) pos);
	}
	else {
		/* Pressing Return in this case is equivalent to clicking
		 * on the first title-less track on the track list.
		 */
		for (i = 0; i < (int) s->tot_trks; i++) {
			if (dbp->track[i].title == NULL) {
				cb.item_position = i + 1;
				cb.reason = XmCR_BROWSE_SELECT;
				cb.event = p->event;

				dbprog_trklist_select(
					widgets.dbprog.trk_list,
					(XtPointer) s,
					(XtPointer) &cb
				);
				break;
			}
		}
	}

	for (i = 0; i < (int) s->tot_trks; i++) {
		if (dbp->track[i].title == NULL) {
			ind_pos = i + 1;

			XmListSelectPos(
				widgets.dbprog.trk_list,
				ind_pos,
				False
			);
			XmListSetBottomPos(
				widgets.dbprog.trk_list,
				ind_pos
			);
			break;
		}
	}
}


/*
 * dbprog_pgmseq_verify
 *	Play sequence editor text widget user-input verification callback.
 */
/*ARGSUSED*/
void
dbprog_pgmseq_verify(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmTextVerifyCallbackStruct
		*p = (XmTextVerifyCallbackStruct *)(void *) call_data;
	int	i;
	char	prev,
		*currstr;

	if (p->reason != XmCR_MODIFYING_TEXT_VALUE)
		return;

	p->doit = True;

	if (p->startPos != p->endPos)
		/* Deleting text, no verification needed */
		return;

	currstr = XmTextGetString(w);

	switch (p->text->format) {
	case XmFMT_8_BIT:
		if (p->currInsert > 0 && currstr != NULL)
			prev = currstr[p->currInsert - 1];
		else
			prev = ',';

		for (i = 0; i < p->text->length; i++) {
			/* Only allowed input is digits, ',' or ' ' */
			if (p->text->ptr[i] == ',' ||
			    p->text->ptr[i] == ' ') {
				if (prev == ',') {
					p->doit = False;
					break;
				}
				/* Substitute ' ' with ',' */
				if (p->text->ptr[i] == ' ')
					p->text->ptr[i] = ',';
			}
			else if (!isdigit((int) p->text->ptr[i])) {
				p->doit = False;
				break;
			}
			prev = p->text->ptr[i];
		}
		break;

	case XmFMT_16_BIT:
	default:
		/* Don't know how to handle other character sets yet */
		p->doit = False;
		break;
	}

	if (currstr != NULL)
		XtFree(currstr);
}


/*
 * dbprog_pgmseq_txtchg
 *	Play sequence editor text widget text changed callback.
 */
/*ARGSUSED*/
void
dbprog_pgmseq_txtchg(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmAnyCallbackStruct	*p = (XmAnyCallbackStruct *)(void *) call_data;
	curstat_t		*s = (curstat_t *)(void *) client_data;
	char			*cp;

	if (p->reason != XmCR_VALUE_CHANGED)
		return;

	if (s->onetrk_prog) {
		/* Currently playing a single-track program: clear it first */
		s->onetrk_prog = FALSE;
		dbprog_progclear(s);
	}

	/* Disable shuffle mode */
	if (s->shuffle) {
		di_shuffle(s, FALSE);
		set_shuffle_btn(FALSE);
	}

	cp = XmTextGetString(w);
	if (!util_newstr(&dbp->playorder, cp)) {
		CD_FATAL(app_data.str_nomemory);
		return;
	}
	if (cp != NULL)
		XtFree(cp);

	if ((s->program = dbprog_pgm_active()) == FALSE)
		dbprog_progclear(s);

	/* Update display */
	dpy_progmode(s, FALSE);

	XtSetSensitive(widgets.dbprog.clrpgm_btn, (Boolean) s->program);
	XtSetSensitive(widgets.dbprog.savepgm_btn, (Boolean) s->program);
}


/*
 * dbprog_addpgm
 *	Program Add button callback.
 */
/*ARGSUSED*/
void
dbprog_addpgm(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	char		tmpbuf[8];

	if (sel_pos < 0 || !di_check_disc(s) || s->mode == MOD_BUSY ||
	    s->mode == MOD_NODISC) {
		cd_beep();
		return;
	}

	if (s->onetrk_prog) {
		/* Currently playing a single-track program: clear it first */
		dbprog_progclear(s);
	}

	/* Disable shuffle mode */
	if (s->shuffle) {
		di_shuffle(s, FALSE);
		set_shuffle_btn(FALSE);
	}

	if (dbp->playorder != NULL && dbp->playorder[0] == '\0') {
		MEM_FREE(dbp->playorder);
		dbp->playorder = NULL;
	}

	if (dbp->playorder == NULL) {
		(void) sprintf(tmpbuf, "%u", s->trkinfo[sel_pos-1].trkno);
		if (!util_newstr(&dbp->playorder, tmpbuf)) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
	}
	else {
		(void) sprintf(tmpbuf, ",%u", s->trkinfo[sel_pos-1].trkno);
		dbp->playorder = (char *) MEM_REALLOC(
			"addpgm",
			dbp->playorder,
			strlen(dbp->playorder) + strlen(tmpbuf) + 1
		);
		if (dbp->playorder == NULL) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
		(void) strcat(dbp->playorder, tmpbuf);
	}

	if (!s->onetrk_prog) {
		XmTextSetString(widgets.dbprog.pgmseq_txt, dbp->playorder);
		XmTextSetInsertionPosition(
			widgets.dbprog.pgmseq_txt,
			strlen(dbp->playorder)
		);
	}

	s->program = TRUE;

	/* Update display */
	dpy_progmode(s, FALSE);

	if (!s->onetrk_prog) {
		XtSetSensitive(widgets.dbprog.clrpgm_btn, True);
		XtSetSensitive(widgets.dbprog.savepgm_btn, True);
	}

	XtSetSensitive(widgets.dbprog.addpgm_btn, False);
	XtSetSensitive(widgets.dbprog.extt_btn, False);
	XtSetSensitive(widgets.dbprog.tcredits_btn, False);

	/* Put focus on clear button */
	XmProcessTraversal(widgets.dbprog.clrpgm_btn, XmTRAVERSE_CURRENT);

	/* Update button labels */
	dbprog_extt_lblupd(NULL);
	dbprog_tcred_lblupd(NULL);

	if (sel_pos > 0) {
		XmListDeselectPos(widgets.dbprog.trk_list, sel_pos);
		sel_pos = -1;
	}

	XmTextSetString(widgets.dbprog.ttitle_txt, "");
}


/*
 * dbprog_clrpgm
 *	Program Clear button callback.
 */
/*ARGSUSED*/
void
dbprog_clrpgm(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;

	if (s->onetrk_prog) {
		/* Currently playing a single-track program: clear it first */
		s->onetrk_prog = FALSE;
		dbprog_progclear(s);
	}
	else if (sel_pos > 0) {
		XmListDeselectPos(widgets.dbprog.trk_list, sel_pos);
		sel_pos = -1;
		XmTextSetString(widgets.dbprog.ttitle_txt, "");
	}

	XmTextSetString(widgets.dbprog.pgmseq_txt, "");
	XtSetSensitive(widgets.dbprog.addpgm_btn, False);
	XtSetSensitive(widgets.dbprog.clrpgm_btn, False);
	XtSetSensitive(widgets.dbprog.savepgm_btn, False);
	XtSetSensitive(widgets.dbprog.extt_btn, False);
	XtSetSensitive(widgets.dbprog.tcredits_btn, False);

	/* Update button labels */
	dbprog_extt_lblupd(NULL);
	dbprog_tcred_lblupd(NULL);

	/* Put focus on OK button */
	XmProcessTraversal(widgets.dbprog.ok_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_savepgm
 *	Program Save button callback.
 */
/*ARGSUSED*/
void
dbprog_savepgm(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	cdinfo_ret_t	ret;

	if (!s->program)
		return;

	/* Save user-defined track program to file */
	if ((ret = cdinfo_save_prog()) != 0) {
		cd_beep();
		DBGPRN(DBG_CDI)(errfp, "cdinfo_save_prog: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
		return;
	}

	XtSetSensitive(widgets.dbprog.savepgm_btn, False);

	/* Put focus on OK button */
	XmProcessTraversal(widgets.dbprog.ok_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_submit
 *	CDDB Submit pushbutton callback.
 */
/*ARGSUSED*/
void
dbprog_submit(Widget w, XtPointer client_data, XtPointer call_data)
{
	cd_confirm_popup(
		app_data.str_confirm, app_data.str_submit,
		(XtCallbackProc) dbprog_submit_yes, client_data,
		(XtCallbackProc) NULL, NULL
	);
}


/*
 * dbprog_submit_popup
 *	CDDB Submit dialog popup timer function
 */
/*ARGSUSED*/
void
dbprog_submit_popup(Widget w, XtPointer client_data, XtPointer call_data)
{
	dbprog_do_submit_popup();
}


/*
 * dbprog_submit_yes
 *	CDDB Submit dialog "Yes" pushbutton callback.
 */
/*ARGSUSED*/
void
dbprog_submit_yes(Widget w, XtPointer client_data, XtPointer call_data)
{
	(void) dbprog_dbsubmit((curstat_t *)(void *) client_data);
}


/*
 * dbprog_flush
 *	CD information cache FLUSH button callback.
 */
/*ARGSUSED*/
void
dbprog_flush(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	cdinfo_ret_t	ret;

	/* Change to watch cursor */
	cd_busycurs(TRUE, CURS_ALL);

	if ((ret = cdinfo_flush(s)) != 0) {
		cd_beep();
		DBGPRN(DBG_CDI)(errfp, "cdinfo_flush: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
	}

	/* Change to normal cursor */
	cd_busycurs(FALSE, CURS_ALL);
}


/*
 * dbprog_load
 *	CD information RELOAD button callback.
 */
/*ARGSUSED*/
void
dbprog_load(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;

	if (!di_check_disc(s) || s->mode == MOD_BUSY || s->mode == MOD_NODISC) {
		cd_beep();
		return;
	}

	/* Re-load CD information */
	dbprog_dbget(s);
}


/*
 * dbprog_stop_load_yes
 *	CD information STOP LOAD dialog 'yes' button callback.
 */
/*ARGSUSED*/
void
dbprog_stop_load_yes(Widget w, XtPointer client_data, XtPointer call_data)
{
	stopload_active = FALSE;
	cdinfo_load_cancel();
}


/*
 * dbprog_stop_load_no
 *	CD information STOP LOAD dialog 'no' button callback.
 */
/*ARGSUSED*/
void
dbprog_stop_load_no(Widget w, XtPointer client_data, XtPointer call_data)
{
	stopload_active = FALSE;
}


/*
 * dbprog_ok
 *	Pop down CD info/program window.
 */
/*ARGSUSED*/
void
dbprog_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmPushButtonCallbackStruct
			*p = (XmPushButtonCallbackStruct *)(void *) call_data;
	curstat_t	*s = (curstat_t *)(void *) client_data;

	if (XtIsManaged(widgets.dlist.form)) {
		/* Force a popdown of the dlist window */
		dbprog_dlist_cancel(
			widgets.dlist.cancel_btn,
			(XtPointer) s,
			(XtPointer) p
		);
	}
	if (XtIsManaged(widgets.fullname.form)) {
		/* Force a popdown of the fullname window */
		dbprog_fullname_ok(
			widgets.fullname.ok_btn,
			(XtPointer) s,
			(XtPointer) p
		);
	}
	if (XtIsManaged(widgets.dbextd.form)) {
		/* Force a popdown of the disc details window */
		dbprog_extd_ok(
			widgets.dbextd.ok_btn,
			(XtPointer) s,
			(XtPointer) p
		);
	}
	if (XtIsManaged(widgets.dbextt.form)) {
		/* Force a popdown of the track details window */
		dbprog_extt_ok(
			widgets.dbextt.ok_btn,
			(XtPointer) s,
			(XtPointer) p
		);
	}
	if (XtIsManaged(widgets.credits.form)) {
		/* Force a popdown of the credits window */
		dbprog_credits_popdown(
			widgets.credits.ok_btn,
			(XtPointer) s,
			(XtPointer) p
		);
	}
	if (XtIsManaged(widgets.segments.form)) {
		/* Force a popdown of the segments window */
		dbprog_segments_popdown(
			widgets.segments.ok_btn,
			(XtPointer) s,
			(XtPointer) p
		);
	}

	/* Pop down the CD info/program window */
	XtUnmapWidget(XtParent(widgets.dbprog.form));
	XtUnmanageChild(widgets.dbprog.form);
}


/*
 * dbprog_do_clear
 *	Changed-save dialog box pushbuttons callback.
 */
/*ARGSUSED*/
void
dbprog_do_clear(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = curstat_addr();
	word32_t	flags = (word32_t)(unsigned long) client_data;

	dbp->flags &= ~CDINFO_CHANGED;
	s->flags |= (flags & 0xffff);

	/* Clear in-core CD information */
	dbprog_dbclear(s, FALSE);
}


/*
 * dbprog_timedpy
 *	Toggle the time display mode in the track list.
 */
/*ARGSUSED*/
void
dbprog_timedpy(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmRowColumnCallbackStruct	*p =
		(XmRowColumnCallbackStruct *)(void *) call_data;
	XmToggleButtonCallbackStruct	*q;
	curstat_t			*s = (curstat_t *)(void *) client_data;

	if (p == NULL)
		return;

	q = (XmToggleButtonCallbackStruct *)(void *) p->callbackstruct;

	if (!q->set)
		return;

	/* Overload the function of these buttons to also
	 * dump the contents of the cdinfo_incore_t structure
	 * in debug mode
	 */
	if (app_data.debug & DBG_ALL)
		cdinfo_dump_incore(s);

	if (p->widget == widgets.dbprog.tottime_btn) {
		if (time_mode == TIME_TOTAL)
			return;	/* No change */

		time_mode = TIME_TOTAL;
	}
	else if (p->widget == widgets.dbprog.trktime_btn) {
		if (time_mode == TIME_TRACK)
			return;	/* No change */

		time_mode = TIME_TRACK;
	}
	else
		return;	/* Invalid widget */

	if (di_check_disc(s) && s->mode != MOD_BUSY && s->mode != MOD_NODISC)
		/* Update track list with new time display mode */
		dbprog_listupd_all(s, FALSE);
}


/*
 * dbprog_set_changed
 *	Set the flag indicating that the user has made changes to the
 *	in-core CD information.
 */
/*ARGSUSED*/
void
dbprog_set_changed(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmAnyCallbackStruct *p = (XmAnyCallbackStruct *)(void *) call_data;

	if (p->reason != XmCR_VALUE_CHANGED)
		return;

	/* Setup of the track details window is not a user change */
	if (!extt_setup) {
		dbp->flags |= CDINFO_CHANGED;
		if (!app_data.cdinfo_inetoffln)
			XtSetSensitive(widgets.dbprog.submit_btn, True);
	}
}


/*
 * dbprog_fullname
 *	Pop up/down the fullname window.
 */
void
dbprog_fullname(Widget w, XtPointer client_data, XtPointer call_data)
{
	bool_t		user_req = (bool_t)(unsigned long) client_data;
	curstat_t	*s = curstat_addr();
	int		new_mode;
	cdinfo_fname_t	*p;
	XmString	xs;
	Widget		nextw;
	char		*str,
			buf2[32],
			buf[STR_BUF_SZ * 2];
	bool_t		fname_err;

	/* Check existing fullname for correctness, if needed */
	fname_err = !dbprog_fullname_ck(s);
	if (fname_err && user_req && fname_mode != FNAME_NONE)
		return;

	/* Set fullname window mode */
	if (w == widgets.dbprog.fullname_btn) {
		if (user_req && fname_mode == FNAME_DISC) {
			/* Pop down the full name window */
			dbprog_fullname_ok(
				widgets.fullname.ok_btn,
				(XtPointer) s, call_data
			);
			return;
		}
		new_mode = FNAME_DISC;
		p = &dbp->disc.artistfname;
	}
	else if (w == widgets.dbextt.fullname_btn) {
		if (user_req && fname_mode == FNAME_TRACK) {
			/* Pop down the full name window */
			dbprog_fullname_ok(
				widgets.fullname.ok_btn,
				(XtPointer) s, call_data
			);
			return;
		}
		new_mode = FNAME_TRACK;
		p = &dbp->track[extt_pos].artistfname;
	}
	else if (w == widgets.credits.fullname_btn) {
		if (user_req && fname_mode == FNAME_CREDITS) {
			/* Pop down the full name window */
			dbprog_fullname_ok(
				widgets.fullname.ok_btn,
				(XtPointer) s, call_data
			);
			return;
		}
		new_mode = FNAME_CREDITS;
		p = &w_cred.crinfo.fullname;
	}
	else
		/* Invalid widget */
		return;

	if (fname_mode != FNAME_NONE && new_mode != fname_mode) {
		/* Pop down the full name window */
		dbprog_fullname_ok(
			widgets.fullname.ok_btn,
			(XtPointer) s, call_data
		);
	}

	fname_mode = new_mode;

	/* Update fullname window head label */
	switch (fname_mode) {
	case FNAME_DISC:
		(void) sprintf(buf, "%.31s", app_data.str_albumartist);
		break;
	case FNAME_TRACK:
		(void) sprintf(buf2, "%.31s", app_data.str_trackartist);
		(void) sprintf(buf, buf2, s->trkinfo[extt_pos].trkno);
		break;
	case FNAME_CREDITS:
		(void) sprintf(buf, "%.31s", app_data.str_credit);
		break;
	}
	(void) sprintf(buf, "%s\n%.90s", buf, app_data.str_fnameguide);

	xs = XmStringCreateLtoR(buf, XmSTRING_DEFAULT_CHARSET);
	XtVaSetValues(widgets.fullname.head_lbl,
		XmNlabelString, xs,
		NULL
	);
	XmStringFree(xs);

	/* Update fullname widgets */
	if (p->dispname != NULL)
		XmTextSetString(widgets.fullname.dispname_txt, p->dispname);
	else
		XmTextSetString(widgets.fullname.dispname_txt, "");

	if (p->lastname != NULL)
		XmTextSetString(widgets.fullname.lastname_txt, p->lastname);
	else
		XmTextSetString(widgets.fullname.lastname_txt, "");

	if (p->firstname != NULL)
		XmTextSetString(widgets.fullname.firstname_txt, p->firstname);
	else
		XmTextSetString(widgets.fullname.firstname_txt, "");

	if (p->the != NULL) {
		XmToggleButtonSetState(widgets.fullname.the_btn, True, False);
		XmTextSetString(widgets.fullname.the_txt, p->the);
		XtSetSensitive(widgets.fullname.the_txt, True);
	}
	else {
		XmToggleButtonSetState(widgets.fullname.the_btn, False, False);
		XmTextSetString(widgets.fullname.the_txt, "");
		XtSetSensitive(widgets.fullname.the_txt, False);
	}

	/* Set autogen button default state */
	str = dbprog_autoname(p);
	if (p->dispname != NULL && str[0] != '\0' &&
	    strcmp(p->dispname, str) != 0) {
		XmToggleButtonSetState(
			widgets.fullname.autogen_btn, False, False
		);
		XtVaSetValues(
			widgets.fullname.dispname_txt,
			XmNeditable, True,
			XmNcursorPositionVisible, True,
			XmNcursorPosition, 0,
			NULL
		);
	}
	else {
		XmToggleButtonSetState(
			widgets.fullname.autogen_btn, True, False
		);
		XtVaSetValues(
			widgets.fullname.dispname_txt,
			XmNeditable, False,
			XmNcursorPositionVisible, False,
			XmNcursorPosition, 0,
			NULL
		);
	}
	MEM_FREE(str);

	/* Pop up the fullname window.
	 * The dialog has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason
	 * for this is we want to avoid a screen glitch when
	 * we move the window in cd_dialog_setpos(), so we
	 * map the window afterwards.
	 */
	if (!XtIsManaged(widgets.fullname.form)) {
		XtManageChild(widgets.fullname.form);

		/* Set up dialog box position */
		cd_dialog_setpos(XtParent(widgets.fullname.form));

		XtMapWidget(XtParent(widgets.fullname.form));
	}

	/* Put input focus on the appropriate widget */
	if (p->firstname == NULL)
		nextw = widgets.fullname.firstname_txt;
	else if (p->lastname == NULL)
		nextw = widgets.fullname.lastname_txt;
	else
		nextw = widgets.fullname.ok_btn;

	XmProcessTraversal(nextw, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_fullname_autogen
 *	Full name window autogen button callback.
 */
/*ARGSUSED*/
void
dbprog_fullname_autogen(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmToggleButtonCallbackStruct	*p =
		(XmToggleButtonCallbackStruct *)(void *) call_data;
	cdinfo_fname_t	*ent;

	XtVaSetValues(widgets.fullname.dispname_txt,
		XmNeditable, !p->set,
		XmNcursorPositionVisible, !p->set,
		XmNcursorPosition, 0,
		NULL
	);

	switch (fname_mode) {
	case FNAME_DISC:
		ent = &dbp->disc.artistfname;
		break;
	case FNAME_TRACK:
		if (extt_pos < 0)
			return;
		ent = &dbp->track[extt_pos].artistfname;
		break;
	case FNAME_CREDITS:
		ent = &w_cred.crinfo.fullname;
		break;
	default:
		return;
	}

	if (p->set) {
		char	*cp;

		/* Auto-generate the name */
		cp = dbprog_autoname(ent);
		if (cp[0] != '\0')
			XmTextSetString(widgets.fullname.dispname_txt, cp);
		MEM_FREE(cp);

		/* Put focus on firstname field */
		XmProcessTraversal(
			widgets.fullname.firstname_txt,
			XmTRAVERSE_CURRENT
		);
	}
	else {
		/* The Display name is now editable, so put focus on it */
		XmProcessTraversal(
			widgets.fullname.dispname_txt,
			XmTRAVERSE_CURRENT
		);
	}
}


/*
 * dbprog_fullname_ok
 *	Full name window OK button callback.
 */
/*ARGSUSED*/
void
dbprog_fullname_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	Widget		nextw;

	switch (fname_mode) {
	case FNAME_DISC:
		nextw = NULL;
		break;
	case FNAME_TRACK:
		nextw = widgets.dbextt.year_txt;
		break;
	case FNAME_CREDITS:
		nextw = widgets.credits.notes_txt;
		break;
	default:
		return;
	}

	/* Check fullname for correctness */
	if (!dbprog_fullname_ck(s))
		return;

	/* Pop down the fullname popup */
	XtUnmapWidget(XtParent(widgets.fullname.form));
	XtUnmanageChild(widgets.fullname.form);

	if (nextw != NULL)
		/* Put keyboard focus on next widget */
		XmProcessTraversal(nextw, XmTRAVERSE_CURRENT);

	/* Update curfile if needed */
	if (fname_changed && fname_mode == FNAME_DISC)
		dbprog_curfileupd();

	fname_changed = FALSE;
	fname_mode = FNAME_NONE;
}


/*
 * dbprog_genre_sel
 *	Genre selector callback
 */
/*ARGSUSED*/
void
dbprog_genre_sel(Widget w, XtPointer client_data, XtPointer call_data)
{
	cdinfo_genre_t	*p = (cdinfo_genre_t *)(void *) client_data,
			*gp,
			*genrep,
			*subgenrep,
			*genre2p,
			*subgenre2p;
	char		*newid,
			**ent;
	Widget		pw;
	int		pos;

	if (dbp->genrelist == NULL)
		return;

	pw = XtParent(w);
	if (pw == widgets.dbextd.genre_menu[0]) {
		pos = -1;
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = genrep;
		ent = &dbp->disc.genre;
	}
	else if (pw == widgets.dbextd.genre_menu[1]) {
		pos = -1;
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = genre2p;
		ent = &dbp->disc.genre2;
	}
	else if (pw == widgets.dbextt.genre_menu[0]) {
		if (extt_pos < 0)
			return;
		pos = extt_pos;
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = genrep;
		ent = &dbp->track[pos].genre;
	}
	else if (pw == widgets.dbextt.genre_menu[1]) {
		if (extt_pos < 0)
			return;
		pos = extt_pos;
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = genre2p;
		ent = &dbp->track[pos].genre2;
	}
	else
		return;	/* Invalid widget */

	if (p != gp) {
		dbp->flags |= CDINFO_CHANGED;
		if (!app_data.cdinfo_inetoffln)
			XtSetSensitive(widgets.dbprog.submit_btn, True);
	}

	if (*ent != NULL) {
		MEM_FREE(*ent);
		*ent = NULL;
	}

	if (p != NULL) {
		newid = NULL;

		for (genrep = dbp->genrelist; genrep != NULL;
		     genrep = genrep->next) {
			if (strcmp(genrep->id, p->id) != 0)
				continue;

			if (genrep->child != NULL) {
				/* Look for the first "General xxx" where
				 * xxx is the main genre, and set
				 * the default subgenre to it.
				 */
				for (subgenrep = genrep->child;
				     subgenrep != NULL;
				     subgenrep = subgenrep->next) {
					if (strncmp(subgenrep->name,
						    "General ", 8) == 0 &&
					    strcmp(subgenrep->name + 8,
						    genrep->name) == 0)
						break;
				}

				if (subgenrep != NULL) {
					/* Found it */
					newid = subgenrep->id;
				}
				else {
					/* Not found: just set the default
					 * subgenre to the first one in the
					 * list
					 */
					newid = genrep->child->id;
				}
			}
			else {
				/* This shouldn't happen, but in case a
				 * there is associated no subgenres,
				 * then just set it to the primary genre
				 * itself
				 */
				newid = genrep->id;
			}
		}

		if (!util_newstr(ent, newid)) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
	}

	dbprog_genreupd(pos);
}


/*
 * dbprog_subgenre_sel
 *	Subgenre selector callback
 */
/*ARGSUSED*/
void
dbprog_subgenre_sel(Widget w, XtPointer client_data, XtPointer call_data)
{
	cdinfo_genre_t	*p = (cdinfo_genre_t *)(void *) client_data,
			*gp,
			*genrep,
			*subgenrep,
			*genre2p,
			*subgenre2p;
	char		**ent;
	Widget		pw,
			optw,
			nonew;
	int		pos;

	if (dbp->genrelist == NULL)
		return;

	pw = XtParent(w);
	if (pw == widgets.dbextd.subgenre_menu[0]) {
		pos = -1;
		optw = widgets.dbextd.subgenre_opt[0];
		nonew = widgets.dbextd.genre_none_btn[0];
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = subgenrep;
		ent = &dbp->disc.genre;
	}
	else if (pw == widgets.dbextd.subgenre_menu[1]) {
		pos = -1;
		optw = widgets.dbextd.subgenre_opt[1];
		nonew = widgets.dbextd.genre_none_btn[1];
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = subgenre2p;
		ent = &dbp->disc.genre2;
	}
	else if (pw == widgets.dbextt.subgenre_menu[0]) {
		if (extt_pos < 0)
			return;

		pos = extt_pos;
		optw = widgets.dbextt.subgenre_opt[0];
		nonew = widgets.dbextt.genre_none_btn[0];
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = subgenrep;
		ent = &dbp->track[pos].genre;
	}
	else if (pw == widgets.dbextt.subgenre_menu[1]) {
		if (extt_pos < 0)
			return;

		pos = extt_pos;
		optw = widgets.dbextt.subgenre_opt[1];
		nonew = widgets.dbextt.genre_none_btn[1];
		dbprog_curgenre(pos, &genrep, &subgenrep,
				&genre2p, &subgenre2p);
		gp = subgenre2p;
		ent = &dbp->track[pos].genre2;
	}
	else
		return;	/* Invalid widget */

	if (p != gp) {
		dbp->flags |= CDINFO_CHANGED;
		if (!app_data.cdinfo_inetoffln)
			XtSetSensitive(widgets.dbprog.submit_btn, True);
	}

	if (*ent != NULL) {
		MEM_FREE(*ent);
		*ent = NULL;
	}

	if (p == NULL || p->id == NULL) {
		XtVaSetValues(optw, XmNmenuHistory, nonew, NULL);
	}
	else if (!util_newstr(ent, p->id)) {
		CD_FATAL(app_data.str_nomemory);
		return;
	}

	dbprog_genreupd(pos);
}


/*
 * dbprog_role_sel
 *	Role selector callback
 */
/*ARGSUSED*/
void
dbprog_role_sel(Widget w, XtPointer client_data, XtPointer call_data)
{
	cdinfo_role_t		*p = (cdinfo_role_t *)(void *) client_data,
				*q,
				*sr;
	int			i;
	Dimension		x;
	Arg			arg[10];
	static cdinfo_role_t	*save_role = NULL,
				*save_trole = NULL,
				*save_srole = NULL;

	if (dbp->rolelist == NULL)
		return;

	switch (credits_mode) {
	case CREDITS_DISC:
		sr = save_role;
		break;
	case CREDITS_TRACK:
		sr = save_trole;
		break;
	case CREDITS_SEG:
		sr = save_srole;
		break;
	default:
		return;	/* Invalid mode */
	}

	if (cred_pos > 0)
		XtSetSensitive(widgets.credits.mod_btn, True);
	else
		XtSetSensitive(widgets.credits.add_btn, True);

	/* If the primary role changed, re-create the associated
	 * sub-role menu entries
	 */
	if (sr != NULL) {
		for (q = sr->child; q != NULL; q = q->next) {
			/* Delete old sub-role entries */
			if (q->aux != NULL) {
				XtUnmanageChild((Widget) q->aux);
				XtDestroyWidget((Widget) q->aux);
				q->aux = NULL;
			}
		}
	}

	if (p != NULL) {
		for (q = p->child; q != NULL; q = q->next) {
			/* Create new sub-role entries */
			i = 0;
			XtSetArg(arg[i], XmNinitialResourcesPersistent,
				 False); i++;
			XtSetArg(arg[i], XmNshadowThickness, 2); i++;
			q->aux = (void *) XmCreatePushButton(
				widgets.credits.subrole_menu,
				q->name,
				arg,
				i
			);
			XtManageChild((Widget) q->aux);
			register_activate_cb((Widget) q->aux,
					     dbprog_subrole_sel, q);

			if (q == p->child) {
				/* Set default sub-role to the first entry */
				XtVaSetValues(
					widgets.credits.subrole_opt,
					XmNmenuHistory, q->aux,
					NULL
				);
				w_cred.crinfo.role = q;
			}
		}
	}
	else {
		/* Set sub-role selector to "none" */
		XtVaSetValues(
			widgets.credits.subrole_opt,
			XmNmenuHistory, widgets.credits.subrole_none_btn,
			NULL
		);
		w_cred.crinfo.role = NULL;
	}

	switch (credits_mode) {
	case CREDITS_DISC:
		save_role = p;
		break;
	case CREDITS_TRACK:
		save_trole = p;
		break;
	case CREDITS_SEG:
		save_srole = p;
		break;
	default:
		break;
	}

	/* Hack: Force the subrole option menu to resize to match
	 * the selected label.
	 */
	XtVaGetValues(widgets.credits.form, XmNwidth, &x, NULL);
	XtVaSetValues(widgets.credits.form, XmNwidth, x+1, NULL);
	XtVaSetValues(widgets.credits.form, XmNwidth, x, NULL);
}


/*
 * dbprog_subrole_sel
 *	Subrole selector callback
 */
/*ARGSUSED*/
void
dbprog_subrole_sel(Widget w, XtPointer client_data, XtPointer call_data)
{
	cdinfo_role_t	*p = (cdinfo_role_t *)(void *) client_data;

	if (dbp->rolelist == NULL)
		return;

	if (p == NULL) {
		/* "None" selected: set the primary role to "None" too */
		XtVaSetValues(
			widgets.credits.prirole_opt,
			XmNmenuHistory, widgets.credits.prirole_none_btn,
			NULL
		);
		XtCallCallbacks(
			widgets.credits.prirole_none_btn,
			XmNactivateCallback, (XtPointer) NULL
		);
	}

	if (cred_pos > 0)
		XtSetSensitive(widgets.credits.mod_btn, True);
	else
		XtSetSensitive(widgets.credits.add_btn, True);

	w_cred.crinfo.role = p;
}


/*
 * dbprog_extd
 *	Pop up/down the disc details window.
 */
void
dbprog_extd(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	static bool_t	first = TRUE;

	if (XtIsManaged(widgets.dbextd.form)) {
		/* Pop down the Disc details window */
		dbprog_extd_ok(w, client_data, call_data);
		return;
	}

	/* Update the disc title */
	dbprog_set_disc_title(s->cur_disc, dbp->disc.artist, dbp->disc.title);

	/* Pop up the disc details window.
	 * The dialog has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason
	 * for this is we want to avoid a screen glitch when
	 * we move the window in cd_dialog_setpos(), so we
	 * map the window afterwards.
	 */
	XtManageChild(widgets.dbextd.form);
	if (first) {
		first = FALSE;
		/* Set up window position */
		cd_dialog_setpos(XtParent(widgets.dbextd.form));
	}
	XtMapWidget(XtParent(widgets.dbextd.form));

	/* Put input focus on the OK button */
	XmProcessTraversal(widgets.dbextd.ok_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_extd_compilation
 *	Disc details compilation toggle button callback
 */
/*ARGSUSED*/
void
dbprog_extd_compilation(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmToggleButtonCallbackStruct	*p =
		(XmToggleButtonCallbackStruct *)(void *) call_data;

	if (dbp->disc.compilation == p->set)
		/* No change */
		return;

	dbp->disc.compilation = p->set;
	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_extd_ok
 *	Disc details window OK button callback.
 */
/*ARGSUSED*/
void
dbprog_extd_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	/* Pop down the Disc details window */
	XtUnmapWidget(XtParent(widgets.dbextd.form));
	XtUnmanageChild(widgets.dbextd.form);
}


/*
 * dbprog_extt
 *	Pop up/down the track details window.
 */
void
dbprog_extt(Widget w, XtPointer client_data, XtPointer call_data)
{
	bool_t		from_main = (bool_t)(unsigned long) client_data,
			managed;
	curstat_t	*s = curstat_addr();
	static bool_t	first = TRUE;

	managed = (bool_t) XtIsManaged(widgets.dbextt.form);
	if (managed) {
		if (from_main) {
			/* Pop down the Track details window */
			dbprog_extt_ok(w, client_data, call_data);
			return;
		}
		else if (credits_mode != CREDITS_TRACK)
			extt_pos = -1;
	}

	if (sel_pos > 0)
		extt_pos = sel_pos - 1;
	else if (extt_pos < 0)
		return;

	/* Enter track details setup mode */
	extt_setup = TRUE;

	/* Update widgets */
	dbprog_exttupd(s, extt_pos);

	if (!managed) {
		/* Pop up the track details popup.
		 * The dialog has mappedWhenManaged set to False,
		 * so we have to map/unmap explicitly.  The reason
		 * for this is we want to avoid a screen glitch when
		 * we move the window in cd_dialog_setpos(), so we
		 * map the window afterwards.
		 */
		XtManageChild(widgets.dbextt.form);
		if (first) {
			first = FALSE;
			/* Set window position */
			cd_dialog_setpos(XtParent(widgets.dbextt.form));
		}
		XtMapWidget(XtParent(widgets.dbextt.form));

		/* Put input focus on the OK button */
		XmProcessTraversal(
			widgets.dbextt.ok_btn,
			XmTRAVERSE_CURRENT
		);
	}

	/* Exit track details setup mode */
	extt_setup = FALSE;
}


/*
 * dbprog_extt_prev
 *	Track details window and credits window previous track button callback.
 */
/*ARGSUSED*/
void
dbprog_extt_prev(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;

	/* Check full name data and abort if needed */
	if (!dbprog_fullname_ck(s)) {
		cd_beep();
		return;
	}

	/* Put input focus on the OK button */
	if (XtIsManaged(widgets.dbextt.form))
		XmProcessTraversal(widgets.dbextt.ok_btn, XmTRAVERSE_CURRENT);
	if (XtIsManaged(widgets.credits.form))
		XmProcessTraversal(widgets.credits.ok_btn, XmTRAVERSE_CURRENT);

	if (extt_pos <= 0) {
		cd_beep();
		return;
	}

	/* Select the previous track */
	if (sel_pos > 0) {
		XmListDeselectPos(widgets.dbprog.trk_list, sel_pos);
		sel_pos = -1;
	}
	XmListSelectPos(widgets.dbprog.trk_list, extt_pos, True);

	/* Scroll track list if necessary */
	dbprog_list_autoscroll(
		widgets.dbprog.trk_list,
		(int) s->tot_trks,
		extt_pos + 1
	);

	/* Update fullname window if necessary */
	if (fname_mode == FNAME_TRACK) {
		dbprog_fullname(widgets.dbextt.fullname_btn,
				(XtPointer) FALSE, call_data);
	}
	else if (fname_mode == FNAME_CREDITS &&
		 credits_mode == CREDITS_TRACK) {
		dbprog_fullname(widgets.credits.fullname_btn,
				(XtPointer) FALSE, call_data);
	}
}


/*
 * dbprog_extt_next
 *	Track details and credits window next track button callback.
 */
/*ARGSUSED*/
void
dbprog_extt_next(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	int		n;

	/* Check full name data and abort if needed */
	if (!dbprog_fullname_ck(s)) {
		cd_beep();
		return;
	}

	/* Put keyboard focus on the OK button */
	if (XtIsManaged(widgets.dbextt.form))
		XmProcessTraversal(widgets.dbextt.ok_btn, XmTRAVERSE_CURRENT);
	if (XtIsManaged(widgets.credits.form))
		XmProcessTraversal(widgets.credits.ok_btn, XmTRAVERSE_CURRENT);

	if (extt_pos < 0) {
		cd_beep();
		return;
	}

	n = extt_pos + 2;
	if (n > (int) s->tot_trks) {
		cd_beep();
		return;
	}

	/* Select the next track */
	if (sel_pos > 0) {
		XmListDeselectPos(widgets.dbprog.trk_list, sel_pos);
		sel_pos = -1;
	}
	XmListSelectPos(widgets.dbprog.trk_list, n, True);

	/* Scroll track list if necessary */
	dbprog_list_autoscroll(
		widgets.dbprog.trk_list,
		(int) s->tot_trks,
		n
	);

	/* Update fullname window if necessary */
	if (fname_mode == FNAME_TRACK) {
		dbprog_fullname(widgets.dbextt.fullname_btn,
				(XtPointer) FALSE, call_data);
	}
	else if (fname_mode == FNAME_CREDITS &&
		 credits_mode == CREDITS_TRACK) {
		dbprog_fullname(widgets.credits.fullname_btn,
				(XtPointer) FALSE, call_data);
	}
}


/*
 * dbprog_extt_autotrk
 *	Track details window Auto-track button callback.
 */
/*ARGSUSED*/
void
dbprog_extt_autotrk(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmToggleButtonCallbackStruct	*p =
		(XmToggleButtonCallbackStruct *)(void *) call_data;
	curstat_t			*s =
		(curstat_t *)(void *) client_data;

	if (p->reason != XmCR_VALUE_CHANGED)
		return;

	DBGPRN(DBG_UI)(errfp, "\n* AUTO-TRACK: %s\n", p->set ? "On" : "Off");

	auto_trk = (bool_t) p->set;

	/* The auto-track buttons in the trackk details and credits
	 * windows are "ganged" together.
	 */
	if (w == widgets.dbextt.autotrk_btn) {
		XmToggleButtonSetState(
			widgets.credits.autotrk_btn, (Boolean) p->set, False
		);
	}
	else if (w == widgets.credits.autotrk_btn) {
		XmToggleButtonSetState(
			widgets.dbextt.autotrk_btn, (Boolean) p->set, False
		);
	}

	dbprog_extt_autotrk_upd(s, di_curtrk_pos(s) + 1);
}


/*
 * dbprog_extt_ok
 *	Track details window OK button callback.
 */
/*ARGSUSED*/
void
dbprog_extt_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	cdinfo_track_t	*t;

	if (fname_mode == FNAME_TRACK) {
		/* Pop down the full name window */
		dbprog_fullname_ok(
			widgets.fullname.ok_btn,
			client_data, call_data
		);
	}

	if (credits_mode != CREDITS_TRACK)
		extt_pos = -1;

	/* Pop down the Track details window */
	XtUnmapWidget(XtParent(widgets.dbextt.form));
	XtUnmanageChild(widgets.dbextt.form);

	/* Update button labels */
	t = (sel_pos > 0) ? &dbp->track[sel_pos - 1] : NULL;
	dbprog_extt_lblupd(t);
	dbprog_tcred_lblupd(t);
}


/*
 * dbprog_credits_popup
 *	Credits button callback.
 */
/*ARGSUSED*/
void
dbprog_credits_popup(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	static bool_t	first = TRUE;

	if (w == widgets.dbprog.dcredits_btn) {
		if (credits_mode == CREDITS_DISC) {
			/* Already popped up - pop it down */
			dbprog_credits_ok(w, client_data, call_data);
			return;
		}

		credits_mode = CREDITS_DISC;
		dbprog_creditupd(s, -1);

		if (XtIsManaged(widgets.credits.autotrk_btn))
			XtUnmanageChild(widgets.credits.autotrk_btn);
		if (XtIsManaged(widgets.credits.prev_btn))
			XtUnmanageChild(widgets.credits.prev_btn);
		if (XtIsManaged(widgets.credits.next_btn))
			XtUnmanageChild(widgets.credits.next_btn);
	}
	else if (w == widgets.dbprog.tcredits_btn) {
		if (credits_mode == CREDITS_TRACK) {
			/* Already popped up - pop it down */
			dbprog_credits_ok(w, client_data, call_data);
			return;
		}

		if (sel_pos > 0)
			extt_pos = sel_pos - 1;
		else if (extt_pos < 0)
			return;

		credits_mode = CREDITS_TRACK;
		dbprog_creditupd(s, extt_pos);

		if (!XtIsManaged(widgets.credits.autotrk_btn))
			XtManageChild(widgets.credits.autotrk_btn);
		if (!XtIsManaged(widgets.credits.prev_btn))
			XtManageChild(widgets.credits.prev_btn);
		if (!XtIsManaged(widgets.credits.next_btn))
			XtManageChild(widgets.credits.next_btn);
	}
	else if (w == widgets.segments.credits_btn) {
		if (credits_mode == CREDITS_SEG) {
			/* Already popped up - pop it down */
			dbprog_credits_ok(w, client_data, call_data);
			return;
		}

		credits_mode = CREDITS_SEG;
		dbprog_creditupd(s, -1);

		if (XtIsManaged(widgets.credits.autotrk_btn))
			XtUnmanageChild(widgets.credits.autotrk_btn);
		if (XtIsManaged(widgets.credits.prev_btn))
			XtUnmanageChild(widgets.credits.prev_btn);
		if (XtIsManaged(widgets.credits.next_btn))
			XtUnmanageChild(widgets.credits.next_btn);
	}
	else
		return;

	/* Initialize role selectors to "None -> None" */
	XtVaSetValues(widgets.credits.prirole_opt,
		XmNmenuHistory,
		widgets.credits.prirole_none_btn,
		NULL
	);
	XtVaSetValues(widgets.credits.subrole_opt,
		XmNmenuHistory,
		widgets.credits.subrole_none_btn,
		NULL
	);

	/* Pop up credits window.
	 * The dialog has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason
	 * for this is we want to avoid a screen glitch when
	 * we move the window in cd_dialog_setpos(), so we
	 * map the window afterwards.
	 */
	XtManageChild(widgets.credits.form);
	if (first) {
		first = FALSE;
		/* Set up window position */
		cd_dialog_setpos(XtParent(widgets.credits.form));
	}
	XtMapWidget(XtParent(widgets.credits.form));

	/* Put focus on OK button */
	XmProcessTraversal(widgets.credits.ok_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_credits_popdown
 *	Pop down the credits window and do some cleanup.
 *
 * Args:
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
dbprog_credits_popdown(Widget w, XtPointer client_data, XtPointer call_data)
{
	if (fname_mode == FNAME_CREDITS) {
		/* Pop down the full name window
		 * Currently dbprog_fullname_ok doesn't use the call_data
		 * arg, so we pass NULL.  If that changes this needs to
		 * change accordingly.
		 */
		dbprog_fullname_ok(
			widgets.fullname.ok_btn, NULL, client_data
		);
	}

	if (!XtIsManaged(widgets.dbextt.form))
		extt_pos = -1;

	/* Pop down credits window */
	XtUnmapWidget(XtParent(widgets.credits.form));
	XtUnmanageChild(widgets.credits.form);

	if (cred_pos > 0) {
		XmListDeselectPos(widgets.credits.cred_list, cred_pos);
		cred_pos = -1;
	}

	credits_mode = CREDITS_NONE;
}


/*
 * dbprog_credits_select
 *	Credits list selector callback
 */
/*ARGSUSED*/
void
dbprog_credits_select(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmListCallbackStruct	*p =
		(XmListCallbackStruct *)(void *) call_data;
	cdinfo_credit_t		*list,
				*cp;
	int			i;

	if (cred_pos == p->item_position) {
		/* Already selected: de-select it */
		XmListDeselectPos(w, cred_pos);
		cred_pos = -1;

		/* Set role selectors to "None" */
		XtVaSetValues(
			widgets.credits.prirole_opt,
			XmNmenuHistory, widgets.credits.prirole_none_btn,
			NULL
		);
		XtCallCallbacks(
			widgets.credits.prirole_none_btn,
			XmNactivateCallback, (XtPointer) NULL
		);
		XmTextSetString(widgets.credits.name_txt, "");
		XmTextSetString(widgets.credits.notes_txt, "");

		XtSetSensitive(widgets.credits.add_btn, False);
		XtSetSensitive(widgets.credits.del_btn, False);
		XtSetSensitive(widgets.credits.mod_btn, False);

		if (fname_mode == FNAME_CREDITS) {
			XmTextSetString(widgets.fullname.dispname_txt, "");
			XmTextSetString(widgets.fullname.lastname_txt, "");
			XmTextSetString(widgets.fullname.firstname_txt, "");
			XmTextSetString(widgets.fullname.the_txt, "");
			fname_changed = FALSE;
		}
	}
	else {
		switch (credits_mode) {
		case CREDITS_DISC:
			list = dbp->disc.credit_list;
			break;
		case CREDITS_TRACK:
			if (extt_pos < 0)
				return;
			list = dbp->track[extt_pos].credit_list;
			break;
		case CREDITS_SEG:
			list = w_seg.credit_list;
			break;
		default:
			return;
		}

		cred_pos = p->item_position;

		/* Look for matching credits list entry */
		i = 0;
		for (cp = list; cp != NULL; cp = cp->next) {
			if (++i == p->item_position)
				break;
		}

		if (cp == NULL || i != p->item_position) {
			/* Error: this shouldn't happen */
			return;
		}

		/* Set role selectors - this causes callbacks to be called,
		 * and thus the w_cred structure will be appropriately
		 * filled.
		 */

		if (cp->crinfo.role != NULL) {
		    if (cp->crinfo.role->parent != NULL &&
			cp->crinfo.role->parent->aux != NULL) {
			XtVaSetValues(
				widgets.credits.prirole_opt,
				XmNmenuHistory, cp->crinfo.role->parent->aux,
				NULL
			);
			XtCallCallbacks(
				cp->crinfo.role->parent->aux,
				XmNactivateCallback,
				(XtPointer) cp->crinfo.role
			);
		    }
		    if (cp->crinfo.role->aux != NULL) {
			XtVaSetValues(
				widgets.credits.subrole_opt,
				XmNmenuHistory, cp->crinfo.role->aux,
				NULL
			);
			XtCallCallbacks(
				cp->crinfo.role->aux,
				XmNactivateCallback,
				(XtPointer) cp->crinfo.role
			);
		    }
		}

		/* Explicitly copy fullname structure data */
		if (!util_newstr(&w_cred.crinfo.fullname.dispname,
				 cp->crinfo.fullname.dispname)) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
		if (!util_newstr(&w_cred.crinfo.fullname.lastname,
				 cp->crinfo.fullname.lastname)) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
		if (!util_newstr(&w_cred.crinfo.fullname.firstname,
				 cp->crinfo.fullname.firstname)) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}
		if (!util_newstr(&w_cred.crinfo.fullname.the,
				 cp->crinfo.fullname.the)) {
			CD_FATAL(app_data.str_nomemory);
			return;
		}

		/* Set text fields - this causes callbacks to be called,
		 * and thus the w_cred structure will be appropriately
		 * filled.
		 */

		if (cp->crinfo.name != NULL)
			XmTextSetString(widgets.credits.name_txt,
					cp->crinfo.name);
		else
			XmTextSetString(widgets.credits.name_txt, "");

		if (cp->notes != NULL)
			XmTextSetString(widgets.credits.notes_txt, cp->notes);
		else
			XmTextSetString(widgets.credits.notes_txt, "");

		/* Set button sensitivities */
		XtSetSensitive(widgets.credits.add_btn, False);
		XtSetSensitive(widgets.credits.mod_btn, False);
		XtSetSensitive(widgets.credits.del_btn, True);
	}

	if (fname_mode == FNAME_CREDITS) {
		/* Update the full name window */
		dbprog_fullname(
			widgets.credits.fullname_btn,
			(XtPointer) FALSE, call_data
		);
	}
}


/*
 * dbprog_credits_ok
 *	Credits window OK button callback.
 */
/*ARGSUSED*/
void
dbprog_credits_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	if ((w == widgets.credits.ok_btn ||
	     w == widgets.dbprog.dcredits_btn ||
	     w == widgets.dbprog.tcredits_btn ||
	     w == widgets.segments.credits_btn) &&
	    (XtIsSensitive(widgets.credits.add_btn) ||
	     XtIsSensitive(widgets.credits.mod_btn))) {
		cd_confirm_popup(
			app_data.str_confirm,
			app_data.str_discardchg,
			(XtCallbackProc) dbprog_credits_popdown, client_data,
			(XtCallbackProc) NULL, NULL
		);
		return;
	}

	dbprog_credits_popdown(w, client_data, call_data);
}


/*
 * dbprog_credits_add
 *	Credits window Add button callback.
 */
/*ARGSUSED*/
void
dbprog_credits_add(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	cdinfo_credit_t	**list,
			*p,
			*q;
	int		pos,
			n;

	switch (credits_mode) {
	case CREDITS_DISC:
		list = &dbp->disc.credit_list;
		pos = -1;
		break;
	case CREDITS_TRACK:
		if (extt_pos < 0)
			return;
		list = &dbp->track[extt_pos].credit_list;
		pos = extt_pos;
		break;
	case CREDITS_SEG:
		list = &w_seg.credit_list;
		pos = -1;
		break;
	default:
		return;
	}

	/* Check entered data for correctness */
	if (!dbprog_credit_ck(&w_cred, pos, s) || !dbprog_fullname_ck(s))
		return;

	p = (cdinfo_credit_t *)(void *) MEM_ALLOC(
		"cdinfo_credit_t",
		sizeof(cdinfo_credit_t)
	);
	if (p == NULL) {
		CD_FATAL(app_data.str_nomemory);
		return;
	}
	(void) memset(p, 0, sizeof(cdinfo_credit_t));

	/* Copy tmp credit structure content to newly allocated struct */
	q = &w_cred;
	p->crinfo.role = q->crinfo.role;
	p->crinfo.name = q->crinfo.name;
	p->crinfo.fullname.dispname = q->crinfo.fullname.dispname;
	p->crinfo.fullname.lastname = q->crinfo.fullname.lastname;
	p->crinfo.fullname.firstname = q->crinfo.fullname.firstname;
	p->crinfo.fullname.the = q->crinfo.fullname.the;
	p->notes = q->notes;

	/* Clean up tmp credit structure */
	q->crinfo.role = NULL;
	q->crinfo.name = NULL;
	q->crinfo.fullname.dispname = NULL;
	q->crinfo.fullname.lastname = NULL;
	q->crinfo.fullname.firstname = NULL;
	q->crinfo.fullname.the = NULL;
	q->notes = NULL;

	/* Add to incore credits list */
	n = 0;
	if (*list == NULL) {
		/* First entry in list */
		*list = p;
	}
	else {
		/* Look for end of credits list and append to it */
		for (n = 1, q = *list; q->next != NULL; q = q->next, n++)
			;
		q->next = p;
		p->prev = q;
	}
	n++;

	dbprog_creditupd(s, pos);
	dbprog_list_autoscroll(widgets.credits.cred_list, n, n);

	if (credits_mode == CREDITS_SEG) {
		if (seg_pos < 0)
			XtSetSensitive(widgets.segments.add_btn, True);
		else
			XtSetSensitive(widgets.segments.mod_btn, True);
	}

	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_credits_mod
 *	Credits window Modify button callback.
 */
/*ARGSUSED*/
void
dbprog_credits_mod(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	cdinfo_credit_t	*list,
			*p,
			*q;
	int		pos,
			sav_credpos,
			n;

	if (cred_pos < 0)
		return;	/* Invalid */

	switch (credits_mode) {
	case CREDITS_DISC:
		list = dbp->disc.credit_list;
		pos = -1;
		break;
	case CREDITS_TRACK:
		if (extt_pos < 0)
			return;
		list = dbp->track[extt_pos].credit_list;
		pos = extt_pos;
		break;
	case CREDITS_SEG:
		list = w_seg.credit_list;
		pos = -1;
		break;
	default:
		return;
	}

	/* Check entered data for correctness */
	if (!dbprog_credit_ck(&w_cred, pos, s) || !dbprog_fullname_ck(s))
		return;

	for (n = 0, p = list; p != NULL; p = p->next, n++) {
		if ((n+1) != cred_pos)
			continue;

		/* Update appropriate incore credit entry */
		q = &w_cred;
		if (p->crinfo.name != NULL)
			MEM_FREE(p->crinfo.name);
		if (p->crinfo.fullname.dispname != NULL)
			MEM_FREE(p->crinfo.fullname.dispname);
		if (p->crinfo.fullname.lastname != NULL)
			MEM_FREE(p->crinfo.fullname.lastname);
		if (p->crinfo.fullname.firstname != NULL)
			MEM_FREE(p->crinfo.fullname.firstname);
		if (p->crinfo.fullname.the != NULL)
			MEM_FREE(p->crinfo.fullname.the);
		if (p->notes != NULL)
			MEM_FREE(p->notes);

		p->crinfo.role = q->crinfo.role;
		p->crinfo.name = q->crinfo.name;
		p->crinfo.fullname.dispname = q->crinfo.fullname.dispname;
		p->crinfo.fullname.lastname = q->crinfo.fullname.lastname;
		p->crinfo.fullname.firstname = q->crinfo.fullname.firstname;
		p->crinfo.fullname.the = q->crinfo.fullname.the;
		p->notes = q->notes;

		/* Clean up tmp structure */
		q->crinfo.role = NULL;
		q->crinfo.name = NULL;
		q->crinfo.fullname.dispname = NULL;
		q->crinfo.fullname.lastname = NULL;
		q->crinfo.fullname.firstname = NULL;
		q->crinfo.fullname.the = NULL;
		q->notes = NULL;
	}

	sav_credpos = cred_pos;

	dbprog_creditupd(s, pos);
	dbprog_list_autoscroll(widgets.credits.cred_list, n, sav_credpos);

	if (credits_mode == CREDITS_SEG) {
		if (seg_pos < 0)
			XtSetSensitive(widgets.segments.add_btn, True);
		else
			XtSetSensitive(widgets.segments.mod_btn, True);
	}

	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_credits_del
 *	Credits window Delete button callback.
 */
/*ARGSUSED*/
void
dbprog_credits_del(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	cdinfo_credit_t	**list,
			*p;
	int		pos,
			scrollpos,
			i,
			n;

	if (cred_pos < 0)
		return;	/* Invalid */

	switch (credits_mode) {
	case CREDITS_DISC:
		list = &dbp->disc.credit_list;
		pos = -1;
		break;
	case CREDITS_TRACK:
		if (extt_pos < 0)
			return;
		list = &dbp->track[extt_pos].credit_list;
		pos = extt_pos;
		break;
	case CREDITS_SEG:
		list = &w_seg.credit_list;
		pos = -1;
		break;
	default:
		return;
	}

	/* Delete from incore credit list */
	if (cred_pos == 1) {
		p = *list;
		*list = p->next;
		if (p->next != NULL)
			p->next->prev = NULL;
	}
	else {
		i = 0;
		for (p = *list; p != NULL; p = p->next) {
			if (++i < cred_pos)
				continue;

			if (p->next != NULL)
				p->next->prev = p->prev;
			if (p->prev != NULL)
				p->prev->next = p->next;

			break;
		}
	}
	if (p != NULL)
		MEM_FREE(p);

	for (n = 0, p = *list; p != NULL; p = p->next, n++)
		;

	if (n > cred_pos)
		scrollpos = cred_pos;
	else
		scrollpos = n;

	dbprog_creditupd(s, pos);
	dbprog_list_autoscroll(widgets.credits.cred_list, n, scrollpos);

	if (credits_mode == CREDITS_SEG) {
		if (seg_pos < 0)
			XtSetSensitive(widgets.segments.add_btn, True);
		else
			XtSetSensitive(widgets.segments.mod_btn, True);
	}

	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_segments_popup
 *	Credits button callback.
 */
/*ARGSUSED*/
void
dbprog_segments_popup(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	static bool_t	first = TRUE;

	if (XtIsManaged(widgets.segments.form)) {
		/* Already popped up - pop it down */
		dbprog_segments_ok(w, client_data, call_data);
		return;
	}

	/* Pop up segments window.
	 * The dialog has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason
	 * for this is we want to avoid a screen glitch when
	 * we move the window in cd_dialog_setpos(), so we
	 * map the window afterwards.
	 */
	XtManageChild(widgets.segments.form);

	/* Update segment window */
	dbprog_segmentupd(s);

	if (first) {
		first = FALSE;
		/* Set up window position */
		cd_dialog_setpos(XtParent(widgets.segments.form));
	}
	XtMapWidget(XtParent(widgets.segments.form));

	/* Put focus on OK button */
	XmProcessTraversal(widgets.segments.ok_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_segments_popdown
 *	Pop down the segments window and do some cleanup.
 *
 * Args:
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
dbprog_segments_popdown(Widget w, XtPointer client_data, XtPointer call_data)
{
	if (credits_mode == CREDITS_SEG) {
		/* Force a popdown of the credits window */
		dbprog_credits_popdown(
			widgets.credits.ok_btn, client_data, call_data
		);
	}

	/* Pop down segments window */
	XtUnmapWidget(XtParent(widgets.segments.form));
	XtUnmanageChild(widgets.segments.form);

	if (seg_pos > 0) {
		XmListDeselectPos(widgets.segments.seg_list, seg_pos);
		seg_pos = -1;
	}
}


/*
 * dbprog_segments_select
 *	Credits list selector callback
 */
/*ARGSUSED*/
void
dbprog_segments_select(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmListCallbackStruct	*p =
		(XmListCallbackStruct *)(void *) call_data;
	curstat_t		*s = (curstat_t *)(void *) client_data;
	cdinfo_segment_t	*sp;
	cdinfo_credit_t		*cp,
				*cp2,
				*cp3;
	char			*buf;
	int			i,
				st,
				sf,
				et,
				ef;

	if (seg_pos == p->item_position) {
		/* Already selected: de-select it */
		XmListDeselectPos(w, seg_pos);
		seg_pos = -1;

		XmTextSetString(widgets.segments.name_txt, "");
		XmTextSetString(widgets.segments.starttrk_txt, "");
		XmTextSetString(widgets.segments.startfrm_txt, "");
		XmTextSetString(widgets.segments.endtrk_txt, "");
		XmTextSetString(widgets.segments.endfrm_txt, "");
		XmTextSetString(widgets.segments.notes_txt, "");

		for (cp = cp2 = w_seg.credit_list; cp != NULL; cp = cp2) {
			cp2 = cp->next;

			if (cp->crinfo.name != NULL)
				MEM_FREE(cp->crinfo.name);
			if (cp->crinfo.fullname.dispname != NULL)
				MEM_FREE(cp->crinfo.fullname.dispname);
			if (cp->crinfo.fullname.lastname != NULL)
				MEM_FREE(cp->crinfo.fullname.lastname);
			if (cp->crinfo.fullname.firstname != NULL)
				MEM_FREE(cp->crinfo.fullname.firstname);
			if (cp->crinfo.fullname.the != NULL)
				MEM_FREE(cp->crinfo.fullname.the);
			if (cp->notes != NULL)
				MEM_FREE(cp->notes);
			MEM_FREE(cp);
		}
		w_seg.credit_list = NULL;

		XtSetSensitive(widgets.segments.add_btn, False);
		XtSetSensitive(widgets.segments.mod_btn, False);
		XtSetSensitive(widgets.segments.del_btn, False);

		s->segplay = SEGP_NONE;
		dpy_progmode(s, FALSE);
	}
	else {
		seg_pos = p->item_position;

		/* Look for matching segments list entry */
		i = 0;
		for (sp = dbp->disc.segment_list; sp != NULL; sp = sp->next) {
			if (++i == p->item_position)
				break;
		}

		if (sp == NULL || i != p->item_position) {
			/* Error: this shouldn't happen */
			return;
		}

		if (w_seg.name != NULL) {
			MEM_FREE(w_seg.name);
			w_seg.name = NULL;
		}
		if (w_seg.start_track != NULL) {
			MEM_FREE(w_seg.start_track);
			w_seg.start_track = NULL;
		}
		if (w_seg.start_frame != NULL) {
			MEM_FREE(w_seg.start_frame);
			w_seg.start_frame = NULL;
		}
		if (w_seg.end_track != NULL) {
			MEM_FREE(w_seg.end_track);
			w_seg.end_track = NULL;
		}
		if (w_seg.end_frame != NULL) {
			MEM_FREE(w_seg.end_frame);
			w_seg.end_frame = NULL;
		}
		if (w_seg.notes != NULL) {
			MEM_FREE(w_seg.notes);
			w_seg.notes = NULL;
		}
		for (cp = cp2 = w_seg.credit_list; cp != NULL; cp = cp2) {
			cp2 = cp->next;

			if (cp->crinfo.name != NULL)
				MEM_FREE(cp->crinfo.name);
			if (cp->crinfo.fullname.dispname != NULL)
				MEM_FREE(cp->crinfo.fullname.dispname);
			if (cp->crinfo.fullname.lastname != NULL)
				MEM_FREE(cp->crinfo.fullname.lastname);
			if (cp->crinfo.fullname.firstname != NULL)
				MEM_FREE(cp->crinfo.fullname.firstname);
			if (cp->crinfo.fullname.the != NULL)
				MEM_FREE(cp->crinfo.fullname.the);
			if (cp->notes != NULL)
				MEM_FREE(cp->notes);
			MEM_FREE(cp);
		}
		w_seg.credit_list = NULL;

		/* Set text fields - this causes callbacks to be called,
		 * and thus the w_seg structure will be appropriately
		 * filled.
		 */

		if (sp->name != NULL)
			XmTextSetString(widgets.segments.name_txt,
					sp->name);
		else
			XmTextSetString(widgets.segments.name_txt, "");

		if (sp->start_track != NULL)
			XmTextSetString(widgets.segments.starttrk_txt,
					sp->start_track);
		else
			XmTextSetString(widgets.segments.starttrk_txt, "");

		if (sp->start_frame != NULL)
			XmTextSetString(widgets.segments.startfrm_txt,
					sp->start_frame);
		else
			XmTextSetString(widgets.segments.startfrm_txt, "");

		if (sp->end_track != NULL)
			XmTextSetString(widgets.segments.endtrk_txt,
					sp->end_track);
		else
			XmTextSetString(widgets.segments.endtrk_txt, "");

		if (sp->end_frame != NULL)
			XmTextSetString(widgets.segments.endfrm_txt,
					sp->end_frame);
		else
			XmTextSetString(widgets.segments.endfrm_txt, "");

		if (sp->notes != NULL)
			XmTextSetString(widgets.segments.notes_txt, sp->notes);
		else
			XmTextSetString(widgets.segments.notes_txt, "");

		/* Copy credit list to the working seg structure */
		cp3 = NULL;
		for (cp = sp->credit_list; cp != NULL; cp = cp->next) {
			cp2 = (cdinfo_credit_t *)(void *) MEM_ALLOC(
				"cdinfo_credit_t",
				sizeof(cdinfo_credit_t)
			);
			if (cp2 == NULL) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}
			(void) memset(cp2, 0, sizeof(cdinfo_credit_t));

			cp2->crinfo.role = cp->crinfo.role;

			if (!util_newstr(&cp2->crinfo.name, cp->crinfo.name)) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}
			if (!util_newstr(&cp2->crinfo.fullname.dispname,
					 cp->crinfo.fullname.dispname)) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}
			if (!util_newstr(&cp2->crinfo.fullname.lastname,
					 cp->crinfo.fullname.lastname)) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}
			if (!util_newstr(&cp2->crinfo.fullname.firstname,
					 cp->crinfo.fullname.firstname)) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}
			if (!util_newstr(&cp2->crinfo.fullname.the,
					 cp->crinfo.fullname.the)) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}
			if (!util_newstr(&cp2->notes, cp->notes)) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}

			if (w_seg.credit_list == NULL) {
				w_seg.credit_list = cp2;
				cp2->prev = NULL;
			}
			else {
				cp3->next = cp2;
				cp2->prev = cp3;
			}
			cp2->next = NULL;
			cp3 = cp2;
		}

		/* Set button sensitivities */
		XtSetSensitive(widgets.segments.add_btn, False);
		XtSetSensitive(widgets.segments.mod_btn, False);
		XtSetSensitive(widgets.segments.del_btn, True);

		/*
		 * Set a->b mode based on this segment
		 */

		buf = XmTextGetString(widgets.segments.starttrk_txt);
		if (buf != NULL) {
			st = atoi(buf);
			XtFree(buf);
		}
		else
			st = 0;	/* shrug */

		buf = XmTextGetString(widgets.segments.startfrm_txt);
		if (buf != NULL) {
			sf = atoi(buf);
			XtFree(buf);
		}
		else
			sf = 0;	/* shrug */

		buf = XmTextGetString(widgets.segments.endtrk_txt);
		if (buf != NULL) {
			et = atoi(buf);
			XtFree(buf);
		}
		else
			et = 0;	/* shrug */

		buf = XmTextGetString(widgets.segments.endfrm_txt);
		if (buf != NULL) {
			ef = atoi(buf);
			XtFree(buf);
		}
		else
			ef = 0;	/* shrug */

		for (i = 0; i < (int) s->tot_trks; i++) {
			if (s->trkinfo[i].trkno == st)
				break;
		}
		if (i == (int) s->tot_trks)
			i = 0;	/* This shouldn't happen */

		s->bp_startpos_tot.addr = s->trkinfo[i].addr + sf;
		util_blktomsf(
			s->bp_startpos_tot.addr,
			&s->bp_startpos_tot.min,
			&s->bp_startpos_tot.sec,
			&s->bp_startpos_tot.frame,
			MSF_OFFSET
		);

		s->bp_startpos_trk.addr = sf;
		util_blktomsf(
			s->bp_startpos_trk.addr,
			&s->bp_startpos_trk.min,
			&s->bp_startpos_trk.sec,
			&s->bp_startpos_trk.frame,
			MSF_OFFSET
		);

		for (i = 0; i < (int) s->tot_trks; i++) {
			if (s->trkinfo[i].trkno == et)
				break;
		}
		if (i == (int) s->tot_trks) {
			i--;	/* This shouldn't happen */
			ef = s->trkinfo[i+1].addr - s->trkinfo[i].addr - 25;
		}

		s->bp_endpos_tot.addr = s->trkinfo[i].addr + ef;
		util_blktomsf(
			s->bp_endpos_tot.addr,
			&s->bp_endpos_tot.min,
			&s->bp_endpos_tot.sec,
			&s->bp_endpos_tot.frame,
			MSF_OFFSET
		);

		s->bp_endpos_trk.addr = ef;
		util_blktomsf(
			s->bp_endpos_trk.addr,
			&s->bp_endpos_trk.min,
			&s->bp_endpos_trk.sec,
			&s->bp_endpos_trk.frame,
			MSF_OFFSET
		);

		/* Clear any play programs */
		XmTextSetString(widgets.dbprog.pgmseq_txt, "");
		XtSetSensitive(widgets.dbprog.addpgm_btn, False);
		XtSetSensitive(widgets.dbprog.clrpgm_btn, False);
		XtSetSensitive(widgets.dbprog.savepgm_btn, False);
		s->onetrk_prog = FALSE;
		dbprog_progclear(s);

		/* Disable shuffle mode */
		if (s->shuffle) {
			di_shuffle(s, FALSE);
			set_shuffle_btn(FALSE);
		}

		/* Stop playback */
		if (s->mode != MOD_STOP)
			di_stop(s, TRUE);

		/* Set a->b mode */
		s->segplay = SEGP_AB;
		dpy_progmode(s, FALSE);
	}

	/* Update credits window */
	dbprog_creditupd(s, -1);
}


/*
 * dbprog_segments_ok
 *	Credits window OK button callback.
 */
/*ARGSUSED*/
void
dbprog_segments_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	if ((w == widgets.segments.ok_btn ||
	     w == widgets.dbprog.segments_btn) &&
	    (XtIsSensitive(widgets.segments.add_btn) ||
	     XtIsSensitive(widgets.segments.mod_btn))) {
		cd_confirm_popup(
			app_data.str_confirm,
			app_data.str_discardchg,
			(XtCallbackProc) dbprog_segments_popdown, client_data,
			(XtCallbackProc) NULL, NULL
		);
		return;
	}

	dbprog_segments_popdown(w, client_data, call_data);
}


/*
 * dbprog_segments_add
 *	Credits window Add button callback.
 */
/*ARGSUSED*/
void
dbprog_segments_add(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t		*s = (curstat_t *)(void *) client_data;
	cdinfo_segment_t	*p,
				*q;
	int			n;

	if (!dbprog_segment_ck(&w_seg, s))
		return;

	p = (cdinfo_segment_t *)(void *) MEM_ALLOC(
		"cdinfo_segment_t",
		sizeof(cdinfo_segment_t)
	);
	if (p == NULL) {
		CD_FATAL(app_data.str_nomemory);
		return;
	}
	(void) memset(p, 0, sizeof(cdinfo_segment_t));

	/* Copy tmp segment structure content to newly allocated struct */
	q = &w_seg;
	p->name = q->name;
	p->start_track = q->start_track;
	p->start_frame = q->start_frame;
	p->end_track = q->end_track;
	p->end_frame = q->end_frame;
	p->notes = q->notes;
	p->credit_list = q->credit_list;

	/* Clean up work structure */
	q->name = NULL;
	q->start_track = NULL;
	q->start_frame = NULL;
	q->end_track = NULL;
	q->end_frame = NULL;
	q->notes = NULL;
	q->credit_list = NULL;

	/* Add to incore segments list */
	n = 0;
	if (dbp->disc.segment_list == NULL) {
		/* First entry in list */
		dbp->disc.segment_list = p;
	}
	else {
		/* Look for end of segments list and append to it */
		for (n = 1, q = dbp->disc.segment_list; q->next != NULL;
		     q = q->next, n++)
			;
		q->next = p;
		p->prev = q;
	}
	n++;

	dbprog_segmentupd(s);
	dbprog_list_autoscroll(widgets.segments.seg_list, n, n);

	if (credits_mode == CREDITS_SEG)
		dbprog_creditupd(s, -1);

	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_segments_mod
 *	Credits window Modify button callback.
 */
/*ARGSUSED*/
void
dbprog_segments_mod(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t		*s = (curstat_t *)(void *) client_data;
	cdinfo_segment_t	*p,
				*q;
	cdinfo_credit_t		*cp,
				*cp2;
	int			n,
				sav_segpos;

	if (seg_pos < 0)
		return;	/* Invalid */

	if (!dbprog_segment_ck(&w_seg, s))
		return;

	for (n = 0, p = dbp->disc.segment_list; p != NULL; p = p->next, n++) {
		if ((n+1) != seg_pos)
			continue;

		/* Update appropriate incore segment entry */
		q = &w_seg;
		if (p->name != NULL)
			MEM_FREE(p->name);
		if (p->start_track != NULL)
			MEM_FREE(p->start_track);
		if (p->start_frame != NULL)
			MEM_FREE(p->start_frame);
		if (p->end_track != NULL)
			MEM_FREE(p->end_track);
		if (p->end_frame != NULL)
			MEM_FREE(p->end_frame);
		if (p->notes != NULL)
			MEM_FREE(p->notes);
		for (cp = cp2 = p->credit_list; cp != NULL; cp = cp2) {
			cp2 = cp->next;

			if (cp->crinfo.name != NULL)
				MEM_FREE(cp->crinfo.name);
			if (cp->crinfo.fullname.dispname != NULL)
				MEM_FREE(cp->crinfo.fullname.dispname);
			if (cp->crinfo.fullname.lastname != NULL)
				MEM_FREE(cp->crinfo.fullname.lastname);
			if (cp->crinfo.fullname.firstname != NULL)
				MEM_FREE(cp->crinfo.fullname.firstname);
			if (cp->crinfo.fullname.the != NULL)
				MEM_FREE(cp->crinfo.fullname.the);
			if (cp->notes != NULL)
				MEM_FREE(cp->notes);
			MEM_FREE(cp);
		}
		p->credit_list = NULL;

		p->name = q->name;
		p->start_track = q->start_track;
		p->start_frame = q->start_frame;
		p->end_track = q->end_track;
		p->end_frame = q->end_frame;
		p->notes = q->notes;
		p->credit_list = q->credit_list;

		/* Clean up work structure */
		q->name = NULL;
		q->start_track = NULL;
		q->start_frame = NULL;
		q->end_track = NULL;
		q->end_frame = NULL;
		q->notes = NULL;
		q->credit_list = NULL;
	}

	sav_segpos = seg_pos;

	dbprog_segmentupd(s);
	dbprog_list_autoscroll(widgets.segments.seg_list, n, sav_segpos);

	if (credits_mode == CREDITS_SEG)
		dbprog_creditupd(s, -1);

	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_segments_del
 *	Credits window Delete button callback.
 */
/*ARGSUSED*/
void
dbprog_segments_del(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t		*s = (curstat_t *)(void *) client_data;
	cdinfo_segment_t	*p;
	cdinfo_credit_t		*cp,
				*cp2;
	int			i,
				n,
				scrollpos;

	if (seg_pos < 0)
		return;	/* Invalid */

	/* Delete from incore segment list */
	if (seg_pos == 1) {
		p = dbp->disc.segment_list;
		dbp->disc.segment_list = p->next;
		if (p->next != NULL)
			p->next->prev = NULL;
	}
	else {
		i = 0;
		for (p = dbp->disc.segment_list; p != NULL; p = p->next) {
			if (++i < seg_pos)
				continue;

			if (p->next != NULL)
				p->next->prev = p->prev;
			if (p->prev != NULL)
				p->prev->next = p->next;

			break;
		}
	}

	if (p != NULL) {
		if (p->name != NULL)
			MEM_FREE(p->name);
		if (p->start_track != NULL)
			MEM_FREE(p->start_track);
		if (p->start_frame != NULL)
			MEM_FREE(p->start_frame);
		if (p->end_track != NULL)
			MEM_FREE(p->end_track);
		if (p->end_frame != NULL)
			MEM_FREE(p->end_frame);
		if (p->notes != NULL)
			MEM_FREE(p->notes);
		for (cp = cp2 = p->credit_list; cp != NULL; cp = cp2) {
			cp2 = cp->next;

			if (cp->crinfo.name != NULL)
				MEM_FREE(cp->crinfo.name);
			if (cp->crinfo.fullname.dispname != NULL)
				MEM_FREE(cp->crinfo.fullname.dispname);
			if (cp->crinfo.fullname.lastname != NULL)
				MEM_FREE(cp->crinfo.fullname.lastname);
			if (cp->crinfo.fullname.firstname != NULL)
				MEM_FREE(cp->crinfo.fullname.firstname);
			if (cp->crinfo.fullname.the != NULL)
				MEM_FREE(cp->crinfo.fullname.the);
			if (cp->notes != NULL)
				MEM_FREE(cp->notes);
			MEM_FREE(cp);
		}

		MEM_FREE(p);
	}

	for (n = 0, p = dbp->disc.segment_list; p != NULL; p = p->next, n++)
		;

	if (n > seg_pos)
		scrollpos = seg_pos;
	else
		scrollpos = n;

	dbprog_segmentupd(s);
	dbprog_list_autoscroll(widgets.segments.seg_list, n, scrollpos);

	if (credits_mode == CREDITS_SEG)
		dbprog_creditupd(s, -1);

	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_regionsel_popup
 *	Region change button callback.
 */
/*ARGSUSED*/
void
dbprog_regionsel_popup(Widget w, XtPointer client_data, XtPointer call_data)
{
	int		i;
	cdinfo_region_t	*rp;
	char		*region;

	if (w == widgets.dbextd.region_chg_btn) {
		reg_mode = REGION_DISC;
		region = dbp->disc.region;
	}
	else if (w == widgets.userreg.region_chg_btn) {
		reg_mode = REGION_USERREG;
		region = dbp->userreg.region;
	}
	else
		return;

	/* Pre-select the current region if any */
	if (region != NULL) {
		rp = dbp->regionlist;
		for (i = 1; rp != NULL; i++, rp = rp->next) {
			if (strcmp(region, rp->id) == 0) {
				/* Pre-select the current region */
				XmListSelectPos(widgets.regionsel.region_list,
						i, False);
				dbprog_list_autoscroll(
					widgets.regionsel.region_list,
					reg_cnt, i
				);
				break;
			}
		}
	}

	/* Pop up the region selector window.
	 * The dialog has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason
	 * for this is we want to avoid a screen glitch when
	 * we move the window in cd_dialog_setpos(), so we
	 * map the window afterwards.
	 */
	if (!XtIsManaged(widgets.regionsel.form)) {
		XtManageChild(widgets.regionsel.form);

		/* Set up dialog box position */
		cd_dialog_setpos(XtParent(widgets.regionsel.form));

		XtMapWidget(XtParent(widgets.regionsel.form));

		/* Put focus on the region list */
		XmProcessTraversal(
			widgets.regionsel.region_list, XmTRAVERSE_CURRENT
		);
	}
}


/*
 * dbprog_regionsel_select
 *	Region selection list callback.
 */
/*ARGSUSED*/
void
dbprog_regionsel_select(Widget w, XtPointer client_data, XtPointer call_data)
{
	int			i;
	XmListCallbackStruct	*p =
				(XmListCallbackStruct *)(void *) call_data;
	cdinfo_region_t		*rp;
	char			**fptr;

	if (p->reason != XmCR_BROWSE_SELECT)
		return;

	if (reg_mode == REGION_DISC)
		fptr = &dbp->disc.region;
	else if (reg_mode == REGION_USERREG)
		fptr = &dbp->userreg.region;
	else
		return;

	/* Look for matching region list entry */
	i = 0;
	for (rp = dbp->regionlist; rp != NULL; rp = rp->next) {
		if (++i == p->item_position)
			break;
	}

	if (rp == NULL || rp->id == NULL || i != p->item_position) {
		/* Error: this shouldn't happen */
		return;
	}

	if (*fptr != NULL && strcmp(rp->id, *fptr) == 0) {
		/* Not changed - de-select the entry */
		XmListDeselectPos(w, p->item_position);

		MEM_FREE(*fptr);
		*fptr = NULL;

		if (reg_mode == REGION_DISC)
			XmTextSetString(widgets.dbextd.region_txt, "");
		else if (reg_mode == REGION_USERREG)
			XmTextSetString(widgets.userreg.region_txt, "");
		return;
	}

	if (reg_mode == REGION_DISC) {
		dbp->flags |= CDINFO_CHANGED;
		if (!app_data.cdinfo_inetoffln)
			XtSetSensitive(widgets.dbprog.submit_btn, True);
	}

	if (!util_newstr(fptr, rp->id)) {
		CD_FATAL(app_data.str_nomemory);
		return;
	}

	if (reg_mode == REGION_DISC)
		XmTextSetString(widgets.dbextd.region_txt, rp->name);
	else if (reg_mode == REGION_USERREG)
		XmTextSetString(widgets.userreg.region_txt, rp->name);
}


/*
 * dbprog_regionsel_ok
 *	Region selection window OK button callback.
 */
/*ARGSUSED*/
void
dbprog_regionsel_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	/* Pop down the region selector popup dialog */
	XtUnmapWidget(XtParent(widgets.regionsel.form));
	XtUnmanageChild(widgets.regionsel.form);

	/* Put the input focus on the next widget */
	if (reg_mode == REGION_DISC) {
		XmProcessTraversal(
			widgets.dbextd.notes_txt,
			XmTRAVERSE_CURRENT
		);
	}
	else if (reg_mode == REGION_USERREG) {
		XmProcessTraversal(
			widgets.userreg.postal_txt,
			XmTRAVERSE_CURRENT
		);
	}

	reg_mode = REGION_NONE;
}


/*
 * dbprog_matchsel_select
 *	Match selection list callback.
 */
/*ARGSUSED*/
void
dbprog_matchsel_select(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmListCallbackStruct	*p =
				(XmListCallbackStruct *)(void *) call_data;

	if (p->reason != XmCR_BROWSE_SELECT)
		return;

	if (p->item_position == match_cnt)
		dbp->match_tag = 0;	/* User chose "none of the above" */
	else
		dbp->match_tag = (long) p->item_position;

	if (!XtIsSensitive(widgets.matchsel.ok_btn))
		XtSetSensitive(widgets.matchsel.ok_btn, True);
}


/*
 * dbprog_matchsel_ok
 *	Match selection window OK button callback.
 */
/*ARGSUSED*/
void
dbprog_matchsel_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	cdinfo_ret_t	ret;

	/* Pop down the match selector popup dialog */
	XtUnmapWidget(XtParent(widgets.matchsel.form));
	XtUnmanageChild(widgets.matchsel.form);

	/* Remove all matchsel list items */
	XmListDeleteAllItems(widgets.matchsel.matchsel_list);
	match_cnt = 0;

	if (dbp->match_tag == 0)
		s->qmode = QMODE_NONE;
	else
		s->qmode = QMODE_WAIT;

	dpy_dbmode(s, FALSE);

	/* Re-query CDDB for the user selected entry */
	if ((ret = cdinfo_load_matchsel(s)) != 0) {
		DBGPRN(DBG_CDI)(errfp,
			"cdinfo_load_matchsel: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));

		/* Set qmode flag */
		s->qmode = QMODE_ERR;
	}
	else if ((dbp->flags & CDINFO_MATCH) != 0) {
		s->qmode = QMODE_MATCH;

		/* Got CDDB data, clear the wwwwarp_cleared flag */
		wwwwarp_cleared = FALSE;
	}
	else
		s->qmode = QMODE_NONE;

	if (stopload_active) {
		/* Pop down the stop load dialog */
		stopload_active = FALSE;
		cd_confirm_popdown();
	}

	/* Update widgets */
	dbprog_structupd(s);

	/* Configure the wwwWarp menu */
	wwwwarp_sel_cfg(s);

	XtSetSensitive(widgets.dbprog.reload_btn, True);

	s->program = dbprog_pgm_active();

	/* Update display */
	dpy_progmode(s, FALSE);
	dpy_dtitle(s);
	dpy_ttitle(s);

	/* Update curfile */
	dbprog_curfileupd();

	/* Add to history and changer lists */
	dbprog_hist_new(s);
	dbprog_chgr_new(s);
}


/*
 * dbprog_the
 *	"The" toggle button callback
 */
/*ARGSUSED*/
void
dbprog_the(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmToggleButtonCallbackStruct	*p =
		(XmToggleButtonCallbackStruct *)(void *) call_data;
	Widget	txtw;

	if (w == widgets.fullname.the_btn)
		txtw = widgets.fullname.the_txt;
	else if (w == widgets.dbextd.the_btn)
		txtw = widgets.dbextd.the_txt;
	else if (w == widgets.dbextt.the_btn)
		txtw = widgets.dbextt.the_txt;
	else {
		cd_beep();
		return;
	}

	if (XtIsSensitive(txtw) == p->set)
		/* No change */
		return;

	XtSetSensitive(txtw, p->set);
	XmTextSetString(txtw, p->set ? app_data.str_the : "");

	/* Put keyboard focus on the The text widget */
	XmProcessTraversal(txtw, XmTRAVERSE_CURRENT);

	dbp->flags |= CDINFO_CHANGED;
	if (!app_data.cdinfo_inetoffln)
		XtSetSensitive(widgets.dbprog.submit_btn, True);
}


/*
 * dbprog_auth_retry
 *	Let the user have the option of retrying the proxy-authorization.
 */
/*ARGSUSED*/
void
dbprog_auth_retry(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = curstat_addr();

	/* Clear out previously entered password */
	if (dbp->proxy_passwd != NULL) {
		(void) memset(dbp->proxy_passwd, 0, strlen(dbp->proxy_passwd));
		MEM_FREE(dbp->proxy_passwd);
		dbp->proxy_passwd = NULL;
	}

	if (client_data != NULL) {
		/* Clear out password field */
		XmTextSetString(widgets.auth.pass_txt, "");

		/* Pop up authorization dialog */
		if (!XtIsManaged(widgets.auth.form)) {
			XtManageChild(widgets.auth.form);
			XtMapWidget(XtParent(widgets.auth.form));
		}

		/* Set keyboard focus to password field */
		XmProcessTraversal(
			widgets.auth.pass_txt,
			XmTRAVERSE_CURRENT
		);
	}
	else {
		/* Auth failed: cannot get CD info */

		/* Set qmode flag */
		s->qmode = QMODE_NONE;

		/* Update widgets */
		dbprog_structupd(s);

		/* Configure the wwwWarp menu */
		wwwwarp_sel_cfg(s);

		XtSetSensitive(widgets.dbprog.reload_btn, True);

		/* Update display */
		dpy_dtitle(s);
		dpy_ttitle(s);

		/* Update curfile */
		dbprog_curfileupd();

		/* Add to history and changer lists */
		dbprog_hist_new(s);
		dbprog_chgr_new(s);
	}
}


/*
 * dbprog_password_vfy
 *	Verify Callback for password text widgets
 */
/*ARGSUSED*/
void
dbprog_password_vfy(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmTextVerifyCallbackStruct
		*p = (XmTextVerifyCallbackStruct *)(void *) call_data;
	char	**fptr,
		*str,
		*tp,
		tc;
	int	i,
		len,
		start;

	if (p->reason != XmCR_MODIFYING_TEXT_VALUE)
		return;

	if (w == widgets.auth.pass_txt)
		fptr = &dbp->proxy_passwd;
	else if (w == widgets.userreg.passwd_txt)
		fptr = &dbp->userreg.passwd;
	else if (w == widgets.userreg.vpasswd_txt)
		fptr = &dbp->userreg.vpasswd;
	else
		return;	/* Invalid widget */

	if (p->text->length >= 1 && p->text->ptr != NULL) {
		if (*fptr == NULL) {
			*fptr = (char *) MEM_ALLOC(
				"passwd",
				p->text->length + 1
			);
			if (*fptr == NULL) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}

			(void) strncpy(*fptr, p->text->ptr, p->text->length);
			(*fptr)[p->text->length] = '\0';
		}
		else {
			len = strlen(*fptr);
			start = (p->startPos < len) ? p->startPos : len;
			tp = *fptr + start;
			tc = *tp;
			*tp = '\0';

			str = (char *) MEM_ALLOC(
				"passwd",
				len + p->text->length + 1
			);
			if (str == NULL) {
				CD_FATAL(app_data.str_nomemory);
				return;
			}

			(void) strcpy(str, *fptr);
			(void) strncat(str, p->text->ptr, p->text->length);
			str[start + p->text->length] = '\0';
			*tp = tc;
			(void) strcat(str, tp);

			(void) memset(*fptr, 0, strlen(*fptr));
			MEM_FREE(*fptr);
			*fptr = str;
		}

		/* Display '*' instead of what the user typed */
		for (i = 0; i < p->text->length; i++)
			p->text->ptr[i] = '*';

		p->doit = True;
	}
	else if (p->text->length == 0) {
		/* backspace */
		if (*fptr != NULL && (*fptr)[0] != '\0') {
			len = strlen(*fptr);

			start = (p->startPos < len) ? p->startPos : (len - 1);
			tp = *fptr + ((p->endPos > len) ? len : p->endPos);

			(*fptr)[start] = '\0';
			(void) strcat(*fptr, tp);
		}

		p->doit = True;
	}
}


/*
 * dbprog_auth_ok
 *	Callback for the proxy-authorization OK button
 */
/*ARGSUSED*/
void
dbprog_auth_ok(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	char		*name;

	if (XtIsManaged(widgets.auth.form)) {
		XtUnmapWidget(XtParent(widgets.auth.form));
		XtUnmanageChild(widgets.auth.form);
	}

	auth_initted = TRUE;

	/* Set proxy auth user name */
	name = XmTextGetString(widgets.auth.name_txt);
	if (dbp->proxy_user != NULL)
		XtFree(dbp->proxy_user);
	dbp->proxy_user = name;

	/* Re-load CD information */
	dbprog_dbget(s);
}


/*
 * dbprog_auth_cancel
 *	Callback for the proxy-authorization Cancel button
 */
/*ARGSUSED*/
void
dbprog_auth_cancel(Widget w, XtPointer client_data, XtPointer call_data)
{
	if (XtIsManaged(widgets.auth.form)) {
		XtUnmapWidget(XtParent(widgets.auth.form));
		XtUnmanageChild(widgets.auth.form);
	}

	dbprog_auth_retry(w, NULL, call_data);
}


/*
 * dbprog_dlist_cancel
 *	Callback for the Disc List Cancel button
 */
/*ARGSUSED*/
void
dbprog_dlist_cancel(Widget w, XtPointer client_data, XtPointer call_data)
{
	XtUnmapWidget(XtParent(widgets.dlist.form));
	XtUnmanageChild(widgets.dlist.form);
}


/*
 * dbprog_dlist
 *	Pop up/down the disc list window.
 */
/*ARGSUSED*/
void
dbprog_dlist(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	static bool_t	first = TRUE;

	if (XtIsManaged(widgets.dlist.form)) {
		/* Pop down the Disc List window */
		dbprog_dlist_cancel(w, client_data, call_data);
		return;
	}

	if (dlist_mode == DLIST_HIST) {
		if (!hist_initted) {
			/* Display the in-core history list */
			dbprog_hist_addall(s);

			hist_initted = TRUE;
		}
	}

	/* Pop up the Disc List window.
	 * The dialog has mappedWhenManaged set to False,
	 * so we have to map/unmap explicitly.  The reason
	 * for this is we want to avoid a screen glitch when
	 * we move the window in cd_dialog_setpos(), so we
	 * map the window afterwards.
	 */
	XtManageChild(widgets.dlist.form);
	if (first) {
		first = FALSE;
		/* Set window position */
		cd_dialog_setpos(XtParent(widgets.dlist.form));
	}
	XtMapWidget(XtParent(widgets.dlist.form));

	/* Put input focus on the cancel button */
	XmProcessTraversal(widgets.dlist.cancel_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_dlist_mode
 *	Disc list type selector menu callback
 */
/*ARGSUSED*/
void
dbprog_dlist_mode(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	int		newmode;

	if (w == widgets.dlist.hist_btn)
		newmode = DLIST_HIST;
	else
		newmode = DLIST_CHGR;

	DBGPRN(DBG_UI)(errfp, "\n* DISC LIST: %s mode selected\n",
		(newmode == DLIST_HIST) ? "History" : "CD Changer");

	if (newmode == dlist_mode)
		/* No change */
		return;

	dlist_mode = newmode;

	/* Clean up the disc list widget */
	XmListDeleteAllItems(widgets.dlist.disc_list);
	dlist_pos = -1;
	hist_cnt = 0;

	XtSetSensitive(widgets.dlist.show_btn, False);
	XtSetSensitive(widgets.dlist.goto_btn, False);
	XtSetSensitive(widgets.dlist.del_btn, False);
	XtSetSensitive(widgets.dlist.delall_btn, False);

	if (dlist_mode == DLIST_HIST) {
		/* Display the in-core history list on the
		 * disc list widget
		 */
		dbprog_hist_addall(s);
	}
	else {
		/* Display the in-core CD changer list on the
		 * disc list widget.
		 */
		dbprog_chgr_addall(s);
	}
}


/*
 * dbprog_dlist_select
 *	Disc list browse selection callback
 */
/*ARGSUSED*/
void
dbprog_dlist_select(Widget w, XtPointer client_data, XtPointer call_data)
{
	XmListCallbackStruct	*p =
		(XmListCallbackStruct *)(void *) call_data;
	cdinfo_dlist_t		*dp;
	int			i;

	if (dlist_pos == p->item_position) {
		/* Already selected: de-select it */
		XmListDeselectPos(w, dlist_pos);
		dlist_pos = -1;

		XtSetSensitive(widgets.dlist.show_btn, False);
		if (dlist_mode == DLIST_HIST)
			XtSetSensitive(widgets.dlist.del_btn, False);
		else if (dlist_mode == DLIST_CHGR)
			XtSetSensitive(widgets.dlist.goto_btn, False);
	}
	else {
		dlist_pos = p->item_position;

		dp = (dlist_mode == DLIST_HIST) ?
			cdinfo_hist_list() : cdinfo_chgr_list();
		for (i = 1; dp != NULL; dp = dp->next, i++) {
			if (i == dlist_pos)
				break;
		}
		if (dp != NULL && dp->device != NULL)
			XtSetSensitive(widgets.dlist.show_btn, True);

		if (dlist_mode == DLIST_HIST)
			XtSetSensitive(widgets.dlist.del_btn, True);
		else if (dlist_mode == DLIST_CHGR)
			XtSetSensitive(widgets.dlist.goto_btn, True);
	}
}


/*
 * dbprog_dlist_show
 *	Show selected disc list entry
 */
/*ARGSUSED*/
void
dbprog_dlist_show(Widget w, XtPointer client_data, XtPointer call_data)
{
	int		i;
	cdinfo_dlist_t	*dp;
	struct tm	*tm;
	char		typstr[16],
			str[FILE_PATH_SZ * 4];

	if (dlist_pos <= 0) {
		/* User has not selected a show target yet */
		cd_beep();
		return;
	}

	dp = (dlist_mode == DLIST_HIST) ?
		cdinfo_hist_list() : cdinfo_chgr_list();
	for (i = 1; dp != NULL; dp = dp->next, i++) {
		if (i == dlist_pos)
			break;
	}

	XmListDeselectPos(widgets.dlist.disc_list, dlist_pos);
	dlist_pos = -1;
	XtSetSensitive(widgets.dlist.show_btn, False);
	XtSetSensitive(widgets.dlist.goto_btn, False);
	XtSetSensitive(widgets.dlist.del_btn, False);

	/* Put input focus on the cancel button */
	XmProcessTraversal(widgets.dlist.cancel_btn, XmTRAVERSE_CURRENT);

	if (dp != NULL) {
		tm = localtime(&dp->time);

		switch (dp->type) {
		case CDINFO_DLIST_LOCAL:
			(void) sprintf(typstr, " (%.10s):",
					app_data.str_local);
			break;
		case CDINFO_DLIST_REMOTE:
		case CDINFO_DLIST_REMOTE1:
			(void) sprintf(typstr, " (%.10s):",
					app_data.str_cddb);
			break;
		default:
			(void) strcpy(typstr, ":");
			break;
		}

		(void) sprintf(str,
		    "%.127s%s%.127s\n\n%s %.63s\n%s %.255s\n%s %d\n%s%s %s",
			(dp->artist == NULL) ? "" : dp->artist,
			(dp->artist != NULL && dp->title != NULL) ? " / " : "",
			(dp->title == NULL) ?
				app_data.str_unkndisc : dp->title,
			"Genre:",
			dp->genre == NULL ?
				"-" : cdinfo_genre_name(dp->genre),
			"Device:",
			dp->device == NULL ? "-" : dp->device,
			"Disc:", dp->discno,
			"Last loaded", typstr,
			dp->time == 0 ? "-" : asctime(tm)
		);
	}
	else {
		/* Error: shouldn't get here */
		cd_beep();
		return;
	}

	/* Pop up information dialog */
	CD_INFO(str);
}


/*
 * dbprog_dlist_goto
 *	Cange to the selected disc list entry
 */
/*ARGSUSED*/
void
dbprog_dlist_goto(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;
	int		newdisc;

	if (dlist_pos <= 0 || dlist_mode != DLIST_CHGR) {
		/* User has not selected a goto target yet,
		 * or wrong list mode.
		 */
		cd_beep();
		return;
	}

	newdisc = dlist_pos;
	XmListDeselectPos(widgets.dlist.disc_list, dlist_pos);
	dlist_pos = -1;
	XtSetSensitive(widgets.dlist.show_btn, False);
	XtSetSensitive(widgets.dlist.goto_btn, False);

	/* Put input focus on the cancel button */
	XmProcessTraversal(widgets.dlist.cancel_btn, XmTRAVERSE_CURRENT);

	if (newdisc == s->cur_disc)
		return;	/* Nothing to do */

	s->prev_disc = s->cur_disc;
	s->cur_disc = newdisc;

	s->flags |= STAT_CHGDISC;

	/* Ask the user if the changed CD information
	 * should be submitted to CDDB.
	 */
	if (!dbprog_chgsubmit(s))
		return;

	s->flags &= ~STAT_CHGDISC;

	/* Change to watch cursor */
	cd_busycurs(TRUE, CURS_ALL);

	/* Do the disc change */
	di_chgdisc(s);

	/* Update display */
	dpy_dbmode(s, FALSE);
	dpy_playmode(s, FALSE);

	/* Change to normal cursor */
	cd_busycurs(FALSE, CURS_ALL);
}


/*
 * dbprog_dlist_delete
 *	Delete selected disc list entry
 */
/*ARGSUSED*/
void
dbprog_dlist_delete(Widget w, XtPointer client_data, XtPointer call_data)
{
	int		i;
	cdinfo_dlist_t	*hp;

	if (dlist_pos <= 0 || dlist_mode != DLIST_HIST) {
		/* User has not selected a delete target yet,
		 * or the list mode is not the history list.
		 */
		cd_beep();
		return;
	}

	/* Delete item from list widget */
	XmListDeletePos(widgets.dlist.disc_list, dlist_pos);

	/* Delete item from in-core history list */
	for (i = 1, hp = cdinfo_hist_list(); hp != NULL; hp = hp->next, i++) {
		if (i == dlist_pos) {
			(void) cdinfo_hist_delent(hp, TRUE);
			break;
		}
	}

	if (hist_cnt > 0)
		hist_cnt--;

	XtSetSensitive(widgets.dlist.show_btn, False);
	XtSetSensitive(widgets.dlist.del_btn, False);
	if (cdinfo_hist_list() == NULL)
		XtSetSensitive(widgets.dlist.delall_btn, False);

	dlist_pos = -1;

	/* Put input focus on the cancel button */
	XmProcessTraversal(widgets.dlist.cancel_btn, XmTRAVERSE_CURRENT);
}


/*
 * dbprog_dlist_delall
 *	Delete all disc list entries
 */
/*ARGSUSED*/
void
dbprog_dlist_delall(Widget w, XtPointer client_data, XtPointer call_data)
{
	if (dlist_mode != DLIST_HIST) {
		/* Delete is supported only for the history list */
		cd_beep();
		return;
	}

	/* Put input focus on the cancel button */
	XmProcessTraversal(widgets.dlist.cancel_btn, XmTRAVERSE_CURRENT);

	/* Pop up confirm dialog */
	cd_confirm_popup(
		app_data.str_confirm, app_data.str_dlist_delall,
		(XtCallbackProc) dbprog_dlist_delall_yes, client_data,
		(XtCallbackProc) NULL, NULL
	);
}


/*
 * dbprog_dlist_delall_yes
 *	Delete all confirmation dialog "Yes" button callback.
 */
/*ARGSUSED*/
void
dbprog_dlist_delall_yes(Widget w, XtPointer client_data, XtPointer call_data)
{
	XtSetSensitive(widgets.dlist.show_btn, False);
	XtSetSensitive(widgets.dlist.del_btn, False);
	XtSetSensitive(widgets.dlist.delall_btn, False);

	/* Delete all items in the list widget */
	XmListDeleteAllItems(widgets.dlist.disc_list);

	/* Delete in-core history list and history file */
	cdinfo_hist_delall(TRUE);

	dlist_pos = -1;
	hist_cnt = 0;
}


/*
 * dbprog_dlist_rescan
 *	Re-scan and re-display disc list entries
 */
/*ARGSUSED*/
void
dbprog_dlist_rescan(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;

	if (dlist_mode == DLIST_HIST) {
		/* Change to watch cursor */
		cd_busycurs(TRUE, CURS_DLIST);

		XtSetSensitive(widgets.dlist.show_btn, False);
		XtSetSensitive(widgets.dlist.goto_btn, False);
		XtSetSensitive(widgets.dlist.del_btn, False);
		XtSetSensitive(widgets.dlist.delall_btn, False);

		/* Delete all items in the list widget */
		XmListDeleteAllItems(widgets.dlist.disc_list);

		/* Delete in-core history list */
		cdinfo_hist_delall(FALSE);

		dlist_pos = -1;
		hist_cnt = 0;

		/* Re-load history file */
		cdinfo_hist_init();

		/* Update list widget */
		dbprog_hist_addall(s);

		/* Change to normal cursor */
		cd_busycurs(FALSE, CURS_DLIST);
	}
	else {
		if (app_data.numdiscs == 1)
			/* Not a changer: nothing to do */
			return;

		switch (s->mode) {
		case MOD_PLAY:
		case MOD_PAUSE:
		case MOD_SAMPLE:
			/* Stop playing first */
			di_stop(s, FALSE);
			break;
		default:
			break;
		}

		/* Make the re-scan button insensitive for now */
		XtSetSensitive(w, False);

		/* Put input focus on the cancel button */
		XmProcessTraversal(
			widgets.dlist.cancel_btn,
			XmTRAVERSE_CURRENT
		);

		/* Change to watch cursor */
		cd_busycurs(TRUE, CURS_ALL);

		if (app_data.numdiscs >= 3) {
			/* Pop up the working dialog */
			cd_working_popup(
				app_data.str_working,
				app_data.str_chgrscan,
				(XtCallbackProc) dbprog_scan_stop_btn,
				(XtPointer) s
			);
		}

		/* Set multiplay mode so we don't block on an empty slot.
		 * Also force reverse mode to be false for scanning.
		 */
		sav_mplay = app_data.multi_play;
		sav_rev = app_data.reverse;
		app_data.multi_play = TRUE;
		app_data.reverse = FALSE;

		/* Start scanning */
		start_slot = s->cur_disc;
		scan_slot = 0;
		s->chgrscan = TRUE;
		dbprog_chgr_scan_next(s);
	}
}


/*
 * dbprog_scan_stop_btn
 *	Rescan working dialog box stop button callback function
 */
/*ARGSUSED*/
void
dbprog_scan_stop_btn(Widget w, XtPointer client_data, XtPointer call_data)
{
	curstat_t	*s = (curstat_t *)(void *) client_data;

	dbprog_chgr_scan_stop(s);
}


/**************** ^^ Callback routines ^^ ****************/


