WFDB Software Package 10.7.0

File: <base>/wave/logpan.c (26,380 bytes)
/* file: logpan.c	G. Moody	 1 May 1990
			Last revised:   24 April 2020
Log panel functions for WAVE

-------------------------------------------------------------------------------
WAVE: Waveform analyzer, viewer, and editor
Copyright (C) 1999 George B. Moody

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, see <http://www.gnu.org/licenses/>.

You may contact the author by e-mail (wfdb@physionet.org) or postal mail
(MIT Room E25-505A, Cambridge, MA 02139 USA).  For updates to this software,
please visit PhysioNet (http://www.physionet.org/).
_______________________________________________________________________________

*/

#include "wave.h"
#include "xvwave.h"
#include <string.h>
#include <X11/Xos.h>		/* for <sys/time.h> */
#include <xview/notice.h>
#include <xview/notify.h>
#include <xview/textsw.h>
#include <xview/defaults.h>

#define LLLMAX	(RNLMAX+40+DSLMAX)	/* max length of line in log file */

Frame log_frame;

/* A WAVE log file contains one-line entries.  Each entry specifies a record
   and a time or time interval in that record, and contains an associated
   text string, which (typically) contains a user-entered description of
   features of the signals at that time.  The format is:
      <record><whitespace><time_spec><whitespace><text>
   where
      <record> is the record name (if empty, use the record name given in the
	previous entry, or the name of the currently displayed record if this
	is the first entry)
      <time_spec> is either the start and end times separated by a hyphen, or
	the time of the center of the region of interest, in strtim format
      <text> is the text of the log entry, which cannot begin with whitespace
	but which may contain embedded whitespace (this field may be empty)
   and <whitespace> is a sequence of one or more space or tab characters.
   This format is compatible with `pschart' and `psfd' script file format.

   The log is kept internally as a doubly-linked list of log_entry structures.
*/

struct log_entry {
    struct log_entry *prev, *next;
    char *record, *time_spec, *text;
} *first_entry, *current_entry, *last_entry;

/* This function selects an annotation at a specified time.  If no such
   annotation exists, it moves the log marker (an index mark pseudo-annotation)
   to the specified time and selects the marker.
*/
void set_marker(t)
WFDB_Time t;
{
    static struct ap *log_marker;

    if (locate_annotation(t, 0))
	attached = annp;
    else if (log_marker) {
	move_annotation(log_marker, t);
	attached = log_marker;
    }
    else if (log_marker = get_ap()) {
	log_marker->this.time = t;
	log_marker->this.anntyp = INDEX_MARK;
	insert_annotation(log_marker);
	attached = log_marker;
    }
}

/* add_entry allocates memory for a log_entry structure, fills it in
   using the input arguments, and inserts it in the linked list at the
   location after the current entry.  (If current_entry is NULL, the
   new entry is inserted at the head of the list.)  Upon successful
   (non-zero) return, current_entry points to the newly inserted
   entry, and first_entry and last_entry may have been updated; if
   memory cannot be allocated, add_entry returns 0, and the log entry
   pointers are left unchanged. */

int add_entry(recp, timep, textp)
char *recp, *timep, *textp; /* record name, time specification, and text */
{
    struct log_entry *new_entry;
    char *p;

    /* Allocate memory for structure and strings. */
    if ((new_entry = (struct log_entry *)malloc(sizeof(struct log_entry)))
	== NULL ||
	(new_entry->record = (char *)malloc(strlen(recp)+1)) == NULL ||
	(new_entry->time_spec = (char *)malloc(strlen(timep)+1)) == NULL ||
	(textp &&
	 (new_entry->text = (char *)malloc(strlen(textp)+1)) == NULL)) {
#ifdef NOTICE
	Xv_notice notice = xv_create((Frame)frame, NOTICE,
				     XV_SHOW, TRUE,
#else
	(void)notice_prompt((Frame)frame, (Event *)NULL,
#endif
			    NOTICE_MESSAGE_STRINGS,
			    "Error in allocating memory for log\n", 0,
			    NOTICE_BUTTON_YES, "Continue", 0);
#ifdef NOTICE
	xv_destroy_safe(notice);
#endif
	if (new_entry) {
	    if (new_entry->time_spec) free(new_entry->time_spec);
	    if (new_entry->record) free(new_entry->record);
	    free(new_entry);
	}
	return (0);
    }

    /* Fill in the log_entry structure. */
    strcpy(new_entry->record, recp);
    strcpy(new_entry->time_spec, timep);
    if (textp) strcpy(new_entry->text, textp);
    else new_entry->text = NULL;

    /* Insert the new entry into the linked list. */
    if (current_entry) {
	if (current_entry->next)	/* insert into middle of list */
	    (current_entry->next)->prev = new_entry;
	else				/* append at tail */
	    last_entry = new_entry;
	new_entry->next = current_entry->next;
	new_entry->prev = current_entry;
	current_entry->next = new_entry;
    }
    else if (first_entry) {		/* insert at head of list */
	new_entry->next = first_entry;
	new_entry->prev = NULL;
	first_entry->prev = new_entry;
	first_entry = new_entry;
    }
    else {				/* list is empty, initialize it */
	new_entry->next = new_entry->prev = NULL;
	first_entry = last_entry = new_entry;
    }

    current_entry = new_entry;
    p = strchr(timep, '-');
    if (p) *p = '\0';
    return (1);
}

/* delete_entry removes the current entry from the linked list, and resets the
   current_entry pointer so that it points to the next entry in the linked
   list if there is one, or to the previous entry otherwise. */

void delete_entry()
{
    struct log_entry *p;

    if (current_entry) {
	if (current_entry->prev)
	    (current_entry->prev)->next = current_entry->next;
	else				/* deleting entry at head */
	    first_entry = current_entry->next;
	if (p = current_entry->next)
	    (current_entry->next)->prev = current_entry->prev;
	else				/* deleting entry at tail */
	    p = last_entry = current_entry->prev;
	free(current_entry->record);
	free(current_entry->time_spec);
	if (current_entry->text) free(current_entry->text);
	free(current_entry);
	current_entry = p;
    }
}

/* read_log appends the contents of the log file named by its argument to
   the linked list.  It returns 1 if completely successful, 0 if the file
   cannot be read or if not all properly formatted entries can be stored. */

int read_log(logfname)
char *logfname;
{
    char buf[LLLMAX+1], *p, *recp = record, *timep, *textp = NULL, *strtok();
    FILE *logfile;
    int ignore;

    if ((logfile = fopen(logfname, "r")) == NULL)
	return (0);
    while (fgets(buf, LLLMAX, logfile)) {	/* read and parse an entry */
	/* Check that entry is properly formatted -- if not, skip it. */
	for (p = buf, ignore = 0; *p; p++) {
	    if (!isprint(*p) && !isspace(*p)) { ignore = 1; break; }
	}
	if (ignore) continue;
	if (buf[0] != ' ' && buf[0] != '\t') {
	    recp = strtok(buf, " \t");	/* first token is record name */
	    timep = strtok(NULL, " \t\n");/* second token is time spec */
	}
	else			/* record name missing, use previous value */
	    timep = strtok(buf, " \t");	/* first token is time spec */
	if (recp == NULL || timep == NULL) continue;
	/* skip improperly formatted entries */
	textp = strtok(NULL, "\n");	/* remainder of line is text */

	if (add_entry(recp, timep, textp) == 0) {
	    fclose(logfile);
	    return (0);
	}
    }
    fclose(logfile);
    return (1);
}

/* write_log copies the contents of the linked list to the log file named by
   its argument.  It returns 1 if successful, 0 otherwise. */

static int log_changes, save_log_backup;

int write_log(logfname)
char *logfname;
{
    int result;
    struct log_entry *p;
    FILE *logfile;

    /* Do we need to back up ? */
    if (save_log_backup) {
	char backfname[LNLMAX+2];

	sprintf(backfname, "%s~", logfname);
	if (rename(logfname, backfname)) {
#ifdef NOTICE
	    Xv_notice notice = xv_create((Frame)frame, NOTICE,
					 XV_SHOW, TRUE,
					 NOTICE_STATUS, &result,
#else
	    result = notice_prompt((Frame)frame, (Event *)NULL,
#endif
		NOTICE_MESSAGE_STRINGS,
		"Your log cannot be saved unless you remove the file named",
		backfname,
		"",
		"You may attempt to correct this problem from",
		"another window after pressing `Continue', or",
		"you may exit immediately and discard your",
		"changes by pressing `Exit'.", 0,
		NOTICE_BUTTON_YES, "Continue",
		NOTICE_BUTTON_NO, "Exit", NULL);
#ifdef NOTICE
	    xv_destroy_safe(notice);
#endif
	    if (result == NOTICE_YES) return (0);
	    else if (post_changes()) {
		xv_destroy_safe(frame);
		exit(1);
	    }
	    else return (0);
	}
	save_log_backup = 0;
    }
	
    if ((logfile = fopen(logfname, "w")) == NULL) {
#ifdef NOTICE
	Xv_notice notice = xv_create((Frame)frame, NOTICE,
				     XV_SHOW, TRUE,
				     NOTICE_STATUS, &result,
#else
	result = notice_prompt((Frame)frame, (Event *)NULL,
#endif
		    NOTICE_MESSAGE_STRINGS,
		    "Your log cannot be saved until you obtain",
		    "write permission for",
		    logfname,
		    "",
		    "You may attempt to correct this problem from",
		    "another window after pressing `Continue', or",
		    "you may exit immediately and discard your",
		    "changes by pressing `Exit'.", 0,
		    NOTICE_BUTTON_YES, "Continue",
		    NOTICE_BUTTON_NO, "Exit", NULL);
#ifdef NOTICE
	xv_destroy_safe(notice);
#endif
	if (result == NOTICE_YES) return (0);
	else if (post_changes()) {
	    xv_destroy_safe(frame);
	    exit(1);
	}
	else return (0);
    }

    for (p = first_entry; p; p = p->next) {
	fprintf(logfile, "%s %s", p->record, p->time_spec);
	if (p->text) fprintf(logfile, " %s", p->text);
	fprintf(logfile, "\n");
    }

    log_changes = 0;
    fclose(logfile);
    return (1);
}

Panel_item log_name_item, log_text_item, load_button,
    add_button, replace_button, delete_button, edit_button, first_button,
    rreview_button, prev_button, pause_button, next_button, review_button,
    last_button;
struct itimerval timer;

Panel log_panel;

void show_current_entry()
{
    char *p;
    int record_changed = 0;
    WFDB_Time t0;

    if (current_entry) {
	if (current_entry->text)
	    strncpy(description, current_entry->text, DSLMAX);
	else
	    description[0] = '\0';
	if (strcmp(record, current_entry->record)) {
	    set_record_item(current_entry->record);
	    record_changed = 1;
	}
	p = strchr(current_entry->time_spec, '-');
	if (p) *p = '\0';
	if ((t0 = strtim(current_entry->time_spec)) < 0L) t0 = -t0;
	if (p)		/* start and end times are given */
	    *p = '-';		
	else {		/* only one time given -- place mark at that time */
	    set_marker(t0);
	    if ((t0 -= nsamp/2) < 0L) t0 = 0L;
	}
	if (!record_changed) {
	    set_frame_title();
	    (void)find_display_list(t0);
	}
	set_start_time(timstr(t0));
	set_end_time(timstr(t0 + nsamp));
	xv_set(log_text_item, PANEL_VALUE, description, NULL);
	disp_proc(log_text_item, (Event *)NULL);
	if (attached)
	  box((int)((attached->this.time - display_start_time)*tscale),
	    (ann_mode==1 && (unsigned)attached->this.chan < nsig) ?
	    (int)(base[(unsigned)attached->this.chan] + mmy(2)) : abase,
	    1);
    }
}

Notify_value show_next_entry()
{
    char *p;
    WFDB_Time t0;

    if (current_entry->next) current_entry = current_entry->next;
    else current_entry = first_entry;
    show_current_entry();
    if (current_entry->next &&
	strcmp(current_entry->next->record, current_entry->record) == 0) {
	p = strchr(current_entry->next->time_spec, '-');
	if (p) *p = '\0';
	if ((t0 = strtim(current_entry->next->time_spec)) < 0L) t0 = -t0;
	if (p) *p = '-';		/* t1 = strtim(p+1); */
	else if ((t0 -= nsamp/2) < 0L) t0 = 0L;
	(void)find_display_list(t0);
    }
    return (NOTIFY_DONE);
}

Notify_value show_prev_entry()
{
    char *p;
    WFDB_Time t0;

    if (current_entry->prev) current_entry = current_entry->prev;
    else current_entry = last_entry;
    show_current_entry();
    if (current_entry->prev &&
	strcmp(current_entry->prev->record, current_entry->record) == 0) {
	p = strchr(current_entry->prev->time_spec, '-');
	if (p) *p = '\0';
	if ((t0 = strtim(current_entry->prev->time_spec)) < 0L) t0 = -t0;
	if (p) *p = '-';		/* t1 = strtim(p+1); */
	else if ((t0 -= nsamp/2) < 0L) t0 = 0L;
	(void)find_display_list(t0);
    }
    return (NOTIFY_DONE);
}

int review_delay;
int review_in_progress;

void log_review(direction)
int direction;
{
    review_in_progress = direction;
    timer.it_value.tv_sec = timer.it_interval.tv_sec = review_delay;
    if (direction == 1)
	notify_set_itimer_func(log_frame, show_next_entry, ITIMER_REAL,
			       &timer, NULL);
    else
	notify_set_itimer_func(log_frame, show_prev_entry, ITIMER_REAL,
			       &timer, NULL);
}	

void pause_review()
{
    review_in_progress = 0;
    notify_set_itimer_func(log_frame,NOTIFY_FUNC_NULL,ITIMER_REAL,NULL,NULL);
}

/* Handle enabling/disabling log navigation buttons for non-review modes. */
void set_buttons()
{
    xv_set(pause_button, PANEL_INACTIVE, TRUE, 0);
    if (log_file_name) {
	xv_set(load_button, PANEL_INACTIVE, FALSE, 0);
	xv_set(add_button, PANEL_INACTIVE, FALSE, 0);
	xv_set(edit_button, PANEL_INACTIVE, FALSE, 0);
    }
    if (current_entry) {
	xv_set(replace_button, PANEL_INACTIVE, FALSE, 0);
	xv_set(delete_button, PANEL_INACTIVE, FALSE, 0);
	xv_set(first_button, PANEL_INACTIVE, FALSE, 0);
	xv_set(last_button, PANEL_INACTIVE, FALSE, 0);
	xv_set(next_button, PANEL_INACTIVE,
	       current_entry->next ? FALSE : TRUE, 0);
	xv_set(prev_button, PANEL_INACTIVE,
	       current_entry->prev ? FALSE : TRUE, 0);
	xv_set(review_button, PANEL_INACTIVE,
	       (current_entry->next || current_entry->prev) ? FALSE : TRUE, 0);
	xv_set(rreview_button, PANEL_INACTIVE,
	       (current_entry->next || current_entry->prev) ? FALSE : TRUE, 0);
    }
    else {
	xv_set(replace_button, PANEL_INACTIVE, TRUE, 0);
	xv_set(delete_button, PANEL_INACTIVE, TRUE, 0);
	xv_set(first_button, PANEL_INACTIVE, TRUE, 0);
	xv_set(rreview_button, PANEL_INACTIVE, TRUE, 0);
	xv_set(prev_button, PANEL_INACTIVE, TRUE, 0);
	xv_set(next_button, PANEL_INACTIVE, TRUE, 0);
	xv_set(review_button, PANEL_INACTIVE, TRUE, 0);
	xv_set(last_button, PANEL_INACTIVE, TRUE, 0);
    }
}

void disable_buttons()
{
    xv_set(load_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(add_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(replace_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(delete_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(edit_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(first_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(rreview_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(prev_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(pause_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(next_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(review_button, PANEL_INACTIVE, TRUE, 0);
    xv_set(last_button, PANEL_INACTIVE, TRUE, 0);
}

/* Edit the log file. */
void edit_log_file()
{
    char *edit_command, *editor, *getenv();
    int clen, elen, result;

    if ((editor = getenv("EDITOR")) == NULL)
	editor = defaults_get_string("wave.texteditor",
				     "Wave.TextEditor",
				     EDITOR);
    elen = strlen(editor);
    edit_command = malloc(elen + strlen(log_file_name) + 3);
    if (edit_command) {
	sprintf(edit_command, "%s %s\n", editor, log_file_name);
	analyze_proc();
	do_command(edit_command);
	free(edit_command);
    }
}
    
/* Handle selections in the log window. */
static void log_select(item, event)
Panel_item item;
Event *event;
{
    int client_data = (int)xv_get(item, PANEL_CLIENT_DATA);
    char timestring[25];
    switch (client_data) {
      case 'a':		/* add an entry */
	if (attached && display_start_time < attached->this.time &&
	    attached->this.time < display_start_time + nsamp)
	    strcpy(timestring, mstimstr(attached->this.time));
	else {
	    strcpy(timestring, strtok(timstr(display_start_time), " "));
	    strcat(timestring, "-");
	    strcat(timestring, strtok(timstr(display_start_time+nsamp), " "));
	}
	if (add_entry(record, timestring, xv_get(log_text_item,PANEL_VALUE))) {
	    if (++log_changes > 10) write_log(log_file_name);
	    set_buttons();
	}
	break;
      case 'd':		/* delete the current entry */
	delete_entry();
	if (++log_changes > 10) write_log(log_file_name);
	set_buttons();
	show_current_entry();
	break;
      case 'e':		/* edit the log file */
	if (log_file_name) {
	    disable_buttons();
	    if (log_changes > 0) write_log(log_file_name);
	    edit_log_file();
	    /* Clear out the old entries. */
	    for (current_entry = first_entry; current_entry; )
		delete_entry();
	    /* Reinitialize from the new log file. */
	    if (read_log(log_file_name))
		save_log_backup = 1;
	    log_changes = 0;
	    current_entry = first_entry;
	    set_buttons();
	}
	break;
      case 'f':		/* specify log file name */
	/* Do nothing unless the name has been changed. */
	if (strncmp(log_file_name, (char *)xv_get(log_name_item,PANEL_VALUE),
		    LNLMAX)) {
	    /* If edits have been made, write the current log to the old file.
	       If the write fails, reset the file name (after write_log has
	       alerted the user). */
	    if (log_changes && write_log(log_file_name) == 0)
		xv_set(log_name_item, PANEL_VALUE, log_file_name, 0);
	    else {
		/* Clear out the old entries. */
		for (current_entry = first_entry; current_entry; )
		    delete_entry();
		strncpy(log_file_name,
			(char *)xv_get(log_name_item, PANEL_VALUE), LNLMAX);
		/* Reinitialize from the new log file. */
		if (read_log(log_file_name))
		    save_log_backup = 1;
		log_changes = 0;
		current_entry = first_entry;
	    }
	    set_buttons();
	    show_current_entry();
	}
	break;
      case 'l':		/* force reload of log file */
	/* If edits have been made, write the current log to the old file.
	   If the write fails, reset the file name (after write_log has
	   alerted the user). */
	if (log_changes) {
	    char backfname[LNLMAX+2];

	    sprintf(backfname, "%s~", log_file_name);
	    save_log_backup = 0;
	    write_log(backfname);
	}
	/* Clear out the old entries. */
	for (current_entry = first_entry; current_entry; )
	    delete_entry();
	strncpy(log_file_name,
		(char *)xv_get(log_name_item, PANEL_VALUE), LNLMAX);
	/* Reinitialize from the new log file. */
	if (read_log(log_file_name))
	    save_log_backup = 1;
	log_changes = 0;
	current_entry = first_entry;
	set_buttons();
	show_current_entry();
	break;
      case 'p':		/* pause review */
	pause_review();
	set_buttons();
	break;
      case 'r':		/* replace description field of current entry */
	if (current_entry) {
	    char *newtext = (char *)xv_get(log_text_item, PANEL_VALUE);
	    char *newtextp;

	    if (strcmp(newtext, current_entry->text) &&
		(newtextp = (char *)malloc(strlen(newtext)+1))) {
		strcpy(newtextp, newtext);
		free(current_entry->text);
		current_entry->text = newtextp;
		if (++log_changes > 10) write_log(log_file_name);
	    }
	}
	break;
      case 'A':		/* show first entry */
	if (first_entry) {
	    current_entry = first_entry;
	    set_buttons();
	    show_current_entry();
	}
	break;
      case '<':		/* show previous entry */
	if (current_entry->prev) {
	    current_entry = current_entry->prev;
	    set_buttons();
	    show_current_entry();
	}
	break;
      case '>':		/* show next entry */
	if (current_entry->next) {
	    current_entry = current_entry->next;
	    set_buttons();
	    show_current_entry();
	}
	break;
      case 'Z':		/* show last entry */
	if (last_entry) {
	    current_entry = last_entry;
	    set_buttons();
	    show_current_entry();
	}
	break;
      case '+':		/* review log entries */
      case '-':		/* review log entries in reverse order */
	disable_buttons();
	xv_set(pause_button, PANEL_INACTIVE, FALSE, 0);
	log_review(client_data == '+' ? 1 : -1);
	break;
    }
}

static void adjust_delay(item, value)
Panel_item item;
int value;
{
    review_delay = value;
    if (review_in_progress) log_review(review_in_progress);
}

/* Set up log window. */
static void create_log_popup()
{
    int dx;
    Icon icon;

    icon = xv_create(XV_NULL, ICON,
		     ICON_IMAGE, icon_image,
		     ICON_LABEL, "Log",
		     NULL);
    log_frame = xv_create(frame, FRAME_CMD,
			  FRAME_LABEL, "WAVE log",
			  FRAME_ICON, icon, 0);
    log_panel = xv_get(log_frame, FRAME_CMD_PANEL);
    log_name_item = xv_create(log_panel, PANEL_TEXT,
	XV_X, xv_col(log_panel, 0),
	XV_Y, xv_row(log_panel, 0),
	PANEL_DISPLAY_LEVEL, PANEL_CURRENT,
	PANEL_LABEL_STRING, "File: ",
	XV_HELP_DATA, "wave:file.log.file",
	PANEL_VALUE_DISPLAY_LENGTH, 60,
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'f',
	0);
    load_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, "Load",
	XV_HELP_DATA, "wave:file.log.load",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'l',
	PANEL_INACTIVE, TRUE,
	0);
    log_text_item = xv_create(log_panel, PANEL_TEXT,
 	XV_X, xv_col(log_panel, 0),
	XV_Y, xv_row(log_panel, 1),
	PANEL_DISPLAY_LEVEL, PANEL_CURRENT,
	PANEL_LABEL_STRING, "Description: ",
	XV_HELP_DATA, "wave:file.log.description",
	PANEL_VALUE_DISPLAY_LENGTH, 50,
	PANEL_CLIENT_DATA, (caddr_t) '!',  /* used by disp_proc, see annot.c */
	0);
    xv_create(log_panel, PANEL_SLIDER,
	XV_HELP_DATA, "wave:file.log.review_delay",
	PANEL_LABEL_STRING, "Delay:",
	PANEL_DIRECTION, PANEL_HORIZONTAL,
	PANEL_VALUE, 5,
	PANEL_MAX_VALUE, 10,
	PANEL_MIN_VALUE, 1,
	PANEL_SHOW_RANGE, TRUE,
	PANEL_SHOW_VALUE, FALSE,
	PANEL_NOTIFY_PROC, adjust_delay,
	NULL);
    add_button = xv_create(log_panel, PANEL_BUTTON,
        XV_X, xv_col(log_panel, 0),
	XV_Y, xv_row(log_panel, 3),
	PANEL_LABEL_STRING, "Add",
	XV_HELP_DATA, "wave:file.log.add",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'a',
	PANEL_INACTIVE, TRUE,
	0);
    replace_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, "Replace",
	XV_HELP_DATA, "wave:file.log.replace",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'r',
	PANEL_INACTIVE, TRUE,
	0);
    delete_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, "Delete",
	XV_HELP_DATA, "wave:file.log.delete",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'd',
	PANEL_INACTIVE, TRUE,
	0);
    edit_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, "Edit",
	XV_HELP_DATA, "wave:file.log.edit",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'e',
	PANEL_INACTIVE, TRUE,
	0);

    dx = xv_get(log_panel, PANEL_ITEM_X_GAP);
    xv_set(log_panel, PANEL_ITEM_X_GAP, 4*dx, 0);

    first_button = xv_create(log_panel, PANEL_BUTTON,
        XV_X, xv_col(log_panel, 0),
	XV_Y, xv_row(log_panel, 4),
	PANEL_LABEL_STRING, "|<",
	XV_HELP_DATA, "wave:file.log.|<",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'A',
	PANEL_INACTIVE, TRUE,
	0);

    xv_set(log_panel, PANEL_ITEM_X_GAP, dx, 0);

    rreview_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, "<<",
	XV_HELP_DATA, "wave:file.log.<<",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) '-',
	PANEL_INACTIVE, TRUE,
	0);
    prev_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, "<",
	XV_HELP_DATA, "wave:file.log.<",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) '<',
	PANEL_INACTIVE, TRUE,
	0);
    pause_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, "Pause",
	XV_HELP_DATA, "wave:file.log.pause",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'p',
	PANEL_INACTIVE, TRUE,
	0);
    next_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, ">",
	XV_HELP_DATA, "wave:file.log.>",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) '>',
	PANEL_INACTIVE, TRUE,
	0);
    review_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, ">>",
	XV_HELP_DATA, "wave:file.log.>>",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) '+',
	PANEL_INACTIVE, TRUE,
	0);
    last_button = xv_create(log_panel, PANEL_BUTTON,
	PANEL_LABEL_STRING, ">|",
	XV_HELP_DATA, "wave:file.log.>|",
	PANEL_NOTIFY_PROC, log_select,
	PANEL_CLIENT_DATA, (caddr_t) 'Z',
	PANEL_INACTIVE, TRUE,
	0);

    window_fit(log_panel);
    window_fit(log_frame);
    xv_set(log_frame, FRAME_CMD_PUSHPIN_IN, TRUE, 0);
}

int log_popup_active = -1;

/* Make the log popup window appear. */
void show_log()
{
    if (log_popup_active < 0) create_log_popup();
    wmgr_top(log_frame);
    xv_set(log_frame, WIN_MAP, TRUE, 0);
    log_popup_active = 1;
}

/* Update and close the log file. */
void finish_log()
{
    if (log_changes > 0) write_log(log_file_name);
}

/* Enter demonstration mode. */
void start_demo()
{
    char *filename, *p, *title, *getenv();
    int c, r, x, y;
    Frame text_frame;
    Textsw textsw;
    Textsw_status status;
    extern void mode_undo();

    if (filename = malloc(strlen(helpdir) + strlen("wave/demo.txt") + 2)) {
        if ((title = getenv("DEMOTITLE")) == NULL)
	    title = "Demonstration of WAVE";
	if ((p = getenv("DEMOX")) == NULL)
	    x = 10;
	else
	    x = atoi(p);
	if ((p = getenv("DEMOY")) == NULL)
	    y = 700;
	else
	    y = atoi(p);
	if ((p = getenv("DEMOCOLS")) == NULL)
	    c = 80;
	else
	    c = atoi(p);
	if ((p = getenv("DEMOROWS")) == NULL)
	    r = 20;
	else
	    r = atoi(p);
        text_frame = xv_create(frame, FRAME,
			       XV_LABEL, title, XV_X, x, XV_Y, y,
			       WIN_COLUMNS, c, WIN_ROWS, r, 0);
	textsw = (Textsw)xv_create(text_frame, TEXTSW, NULL);
	sprintf(filename, "%s/wave/demo.txt", helpdir);
	xv_set(textsw,
	       TEXTSW_STATUS, &status,
	       TEXTSW_FILE, filename,
	       TEXTSW_FIRST, 0,
	       TEXTSW_READ_ONLY, TRUE,
	       NULL);
	if (status == TEXTSW_STATUS_OKAY)
	    xv_set(text_frame, WIN_MAP, TRUE, 0);
	free(filename);
    }
    create_log_popup();
    log_popup_active = 0;
    xv_set(log_name_item, PANEL_VALUE, log_file_name, NULL);
    show_mode();
    ghflag = gvflag = visible = 1;
    show_signame = 16;
    mode_undo();
    dismiss_mode();
    if (read_log(log_file_name)) {
	xv_set(pause_button, PANEL_INACTIVE, FALSE, 0);
	log_review((Panel_item)NULL, (Event *)NULL);
    }
}