/*
    This file is part of the FElt finite element analysis package.
    Copyright (C) 1993 Jason I. Gobat and Darren C. Atkinson

    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.
*/

/************************************************************************
 * File:	filedialog.c						*
 *									*
 * Description: This file contains the private and public function and	*
 *		type definitions for the file dialog box.		*
 ************************************************************************/

# include <stdio.h>
# include <dirent.h>
# include <X11/Xos.h>
# include <sys/stat.h>
# include <pwd.h>
# include <X11/Intrinsic.h>
# include <X11/StringDefs.h>
# include <X11/Shell.h>
# include <X11/Xaw/AsciiText.h>
# include <X11/Xaw/Command.h>
# include <X11/Xaw/Label.h>
# include <X11/Xaw/List.h>
# include <X11/Xaw/Viewport.h>
# include "Layout.h"
# include "FileDialog.h"
# include "TabGroup.h"
# include "util.h"

# ifndef X_NOT_STDC_ENV
# include <stdlib.h>
# else
extern char *getenv ( );
# endif

# define MaxEntries 1024
# define MaxPathLen 1024

# define Waiting   0
# define Canceled -1
# define Okayed    1


struct file_dialog {
    Widget	   shell;		/* transientShell  <specified>	*/
    Widget	   layout;		/*	Layout  layout		*/
    Widget	   label;		/*	     Label  label	*/
    Widget	   directory;		/*	     Label  directory	*/
    Widget	   entry;		/*	     AsciiText  entry	*/
    Widget	   viewport;		/*	     Viewport  viewport	*/
    Widget	   list;		/*		  List  list	*/
    Widget	   okay;		/*	     Command  okay	*/
    Widget	   cancel;		/*	     Command  cancel	*/
    String	   path;
    XtCallbackProc callback;
    XtPointer	   client_data;
    int		   status;
};


/* Resources */

static Pixel highlight;

static String dummy_list [ ] = {
    "                              ", "", "", "", "", "", "", "", "", "", NULL
};

static char layout_string [ ] =
"vertical { 4 \
 horizontal { 4 label <+inf -100% *> 4 } 4 \
 horizontal { 4 directory <+inf -100% *> 4 } 4 \
 horizontal { 4 entry <+inf -100% *> 4 } 4 \
 horizontal { 4 viewport <+inf -100% * +inf -100%> 4 } 4 \
 horizontal { 4 okay 4 <+inf -100%> cancel 4 } 4 }";

static Arg color_args [ ] = {
    {XtNborderColor, (XtArgVal) &highlight},
};

static Arg layout_args [ ] = {
    {XtNlayout, (XtArgVal) NULL},
};

static Arg label_args [ ] = {
    {XtNresize,      (XtArgVal) True},
    {XtNjustify,     (XtArgVal) XtJustifyLeft},
    {XtNborderWidth, (XtArgVal) 0},
};

static Arg directory_args [ ] = {
    {XtNresize,      (XtArgVal) True},
    {XtNjustify,     (XtArgVal) XtJustifyLeft},
    {XtNborderWidth, (XtArgVal) 0},
};

static Arg entry_args [ ] = {
    {XtNresize,      (XtArgVal) XawtextResizeWidth},
    {XtNeditType,    (XtArgVal) XawtextEdit},
    {XtNborderWidth, (XtArgVal) 0},
};

static Arg viewport_args [ ] = {
    {XtNallowVert,   (XtArgVal) True},
    {XtNuseRight,    (XtArgVal) True},
    {XtNforceBars,   (XtArgVal) True},
    {XtNborderWidth, (XtArgVal) 0},
};

static Arg list_args [ ] = {
    {XtNdefaultColumns, (XtArgVal) 1},
    {XtNforceColumns,   (XtArgVal) 1},
    {XtNresize,         (XtArgVal) True},
    {XtNlist,           (XtArgVal) dummy_list},
};


/* Translation tables */

static String entry_table =
"<Key>Return:   FileDialogOkay()\n\
 <Key>Escape:   FileDialogCancel()\n\
 <Btn1Down>:    SetFocus() select-start()";

static XtTranslations entry_translations;

static String viewport_table =
"<Key>Return:   FileDialogOkay()\n\
 <Key>Escape:   FileDialogCancel()\n\
 <Btn1Down>:    SetFocus()";

static XtTranslations viewport_translations;

static String list_table =
"<Btn2Down>:    Set()\n\
 <Btn2Up>:      Notify() FileDialogOkay()";

static XtTranslations list_translations;

static String okay_table =
"<Key>Return:   AutoRepeat(off) set()\n\
 <KeyUp>Return: AutoRepeat(saved) notify() unset()\n\
 <Key>space:    AutoRepeat(off) set()\n\
 <KeyUp>space:  AutoRepeat(saved) notify() unset()\n\
 <Key>Escape:   FileDialogCancel()";

static XtTranslations okay_translations;

static String cancel_table =
"<Key>Return:   AutoRepeat(off) set()\n\
 <KeyUp>Return: AutoRepeat(saved) notify() unset()\n\
 <Key>space:    AutoRepeat(off) set()\n\
 <KeyUp>space:  AutoRepeat(saved) notify() unset()\n\
 <Key>Escape:   FileDialogCancel()";

static XtTranslations cancel_translations;


/************************************************************************
 * Function:	SortEntries						*
 *									*
 * Description:	Used by qsort() to compare two strings; directories	*
 *		(which end with a /) are always placed before files	*
 ************************************************************************/

static int SortEntries (s1, s2)
    String *s1;
    String *s2;
{
    String dir1;
    String dir2;


    dir1 = strchr (*s1, '/');
    dir2 = strchr (*s2, '/');

    if (dir1 != NULL && dir2 == NULL)
	return -1;

    if (dir1 == NULL && dir2 != NULL)
	return 1;

    return strcmp (*s1, *s2);
}


/************************************************************************
 * Function:	ReadDirectory						*
 *									*
 * Description:	Reads the entries of the specified directory, sorts the	*
 *		entries, and updates the associated list widget with	*
 *		the entries.  Directories are placed before files in	*
 *		sorted order and directory names end with a /.  If the	*
 *		directory cannot be opened, False is returned.		*
 *		Otherwise, True is returned.				*
 ************************************************************************/

static Boolean ReadDirectory (filed, name)
    FileDialog filed;
    String     name;
{
    int		   count;
    DIR		  *dirp;
    struct stat	   buf;
    struct dirent *entry;
    static String  list [MaxEntries] = {NULL};
    char	   buffer [MaxPathLen];


    if ((dirp = opendir (name)) == NULL)
	return False;

    count = 0;
    while ((entry = readdir (dirp)) != NULL && count < MaxEntries) {
	if (!strcmp (entry -> d_name, "."))
	    continue;

	XtFree (list [count]);
	sprintf (buffer, "%s/%s", name, entry -> d_name);
	if (!stat (buffer, &buf) && S_ISDIR (buf.st_mode)) {
	    sprintf (buffer, "%s/", entry -> d_name);
	    list [count ++] = XtNewString (buffer);
	} else
	    list [count ++] = XtNewString (entry -> d_name);
    }

    closedir (dirp);
    qsort (list, count, sizeof (String), SortEntries);

    XawListChange (filed -> list, list, count, 0, True);
    XawViewportSetCoordinates (filed -> viewport, 0, 0);
    return True;
}


/************************************************************************
 * Function:	CopySelected						*
 *									*
 * Description:	A callback for the List widget which copies the current	*
 *		entry to the entry widget.				*
 ************************************************************************/

static void CopySelected (w, client_data, call_data)
    Widget    w;
    XtPointer client_data;
    XtPointer call_data;
{
    FileDialog		 filed;
    XawListReturnStruct *info;


    filed = (FileDialog) client_data;
    info = (XawListReturnStruct *) call_data;
    SetTextString (filed -> entry, info -> string);
}


/************************************************************************
 * Function:	Okay							*
 *									*
 * Description:	A callback for the okay button which forms a path name	*
 *		from the directory and current entry.  If the entry	*
 *		is an absolute path (begins with / or ~) then the	*
 *		current directory is ignored.  The path name is then	*
 *		normalized (removing .., //, and ~).  If the path names	*
 *		a file then that file is selected.  Otherwise, the path	*
 *		is used as a directory which is then read.		*
 ************************************************************************/

static void Okay (w, client_data, call_data)
    Widget    w;
    XtPointer client_data;
    XtPointer call_data;
{
    char	   old;
    char	   buffer [MaxPathLen];
    Arg		   args   [1];
    String	   dest;
    String	   string;
    String	   ptr;
    FileDialog	   filed;
    struct passwd *entry;


    filed = (FileDialog) client_data;

    XtSetArg (args [0], XtNstring, &string);
    XtGetValues (filed -> entry, args, 1);


    /* If the entry is absolute ignore the current directory.  Otherwise,
       concatenate the entry and directory to form the new path. */

    if (string [0] == '/' || string [0] == '~')
	strcpy (buffer, string);
    else {
	strcpy (buffer, filed -> path);
	strcat (buffer, string);
    }


    /* Expand the tilde. */

    ptr = buffer;
    dest = filed -> path;

    if (!strncmp (buffer, "~/", 2) || !strcmp (buffer, "~")) {
	strcpy (dest, getenv ("HOME"));
	dest += strlen (dest);
	ptr ++;

    } else if (buffer [0] == '~') {
	while (*ptr && *ptr != '/')
	    ptr ++;

	old = *ptr;
	*ptr = 0;

	if ((entry = getpwnam (buffer + 1)) != NULL) {
	    strcpy (dest, entry -> pw_dir);
	    dest += strlen (dest);
	    *ptr = old;
	} else
	    ptr = buffer;
    }


    /* Finish normalizing the directory entry removing any .. or //. */

    while (*ptr) {
	if (!strncmp (ptr, "//", 2) || !strcmp (ptr, "/"))
	    ptr ++;

	else if (!strncmp (ptr, "/../", 4) || !strcmp (ptr, "/..")) {
	    while (dest != filed -> path && *dest != '/')
		dest --;
	    ptr +=3;

	} else
	    *dest ++ = *ptr ++;

	*dest = 0;
    }

    if (filed -> path [0] == 0)
	strcpy (filed -> path, "/");


    /* Try to read the normalized path as a directory. */

    if (ReadDirectory (filed, filed -> path) == True) {


	/* Indicate the new directory and an empty entry. */

	if (strcmp (filed -> path, "/"))
	    strcat (filed -> path, "/");

	XtSetArg (args [0], XtNlabel, filed -> path);
	XtSetValues (filed -> directory, args, 1);

	XtSetArg (args [0], XtNstring, "");
	XtSetValues (filed -> entry, args, 1);


    /* A file was selected.  If this is the initial suggestion then decompose
       the suggestion into a directory and file name and use them. */

    } else if (call_data != NULL) {
	ptr = strrchr (filed -> path, '/');

	XtSetArg (args [0], XtNstring, ptr + 1);
	XtSetValues (filed -> entry, args, 1);

	ptr [1] = 0;
	XtSetArg (args [0], XtNlabel, filed -> path);
	XtSetValues (filed -> directory, args, 1);
	ReadDirectory (filed, filed -> path);


    /* A file was selected.  Call the callback or set the
       selected flag, whichever is appropriate. */

    } else if (filed -> callback != NULL)
	filed -> callback (filed -> entry, filed -> client_data, filed -> path);

    else
	filed -> status = Okayed;
}


/************************************************************************
 * Function:	Cancel							*
 *									*
 * Description:	A callback for the cancel button which indicates that	*
 *		the file dialog has been canceled by either calling the	*
 *		registered callback with a NULL string or by setting	*
 *		the canceled flag.					*
 ************************************************************************/

static void Cancel (w, client_data, call_data)
    Widget    w;
    XtPointer client_data;
    XtPointer call_data;
{
    FileDialog filed;


    filed = (FileDialog) client_data;

    if (filed -> callback != NULL)
	filed -> callback (filed -> entry, filed -> client_data, NULL);
    else
	filed -> status = Canceled;
}


/************************************************************************
 * Function:	FileDialogOkay						*
 *									*
 * Description:	An action procedure which emulates pressing of the okay	*
 *		button.							* 
 ************************************************************************/

static void FileDialogOkay (w, event, params, num_params)
    Widget   w;
    XEvent  *event;
    String  *params;
    Cardinal num_params;
{
    if (XtClass (w) == listWidgetClass)
	w = XtParent (w);

    w = XtNameToWidget (XtParent (w), "okay");
    XtCallCallbacks (w, XtNcallback, NULL);
}


/************************************************************************
 * Function:	FileDialogCancel					*
 *									*
 * Description:	An action procedure which emulates pressing of the	*
 *		cancel button.						*
 ************************************************************************/

static void FileDialogCancel (w, event, params, num_params)
    Widget   w;
    XEvent  *event;
    String  *params;
    Cardinal num_params;
{
    if (event -> type == ClientMessage)
	w = XtNameToWidget (w, "layout.cancel");
    else
	w = XtNameToWidget (XtParent (w), "cancel");

    XtCallCallbacks (w, XtNcallback, NULL);
}


/************************************************************************
 * Function:	FileDialogCreate					*
 *									*
 * Description:	Creates and returns a new file dialog box.  The dialog	*
 *		can be popped up using FileDialogSelect() which forces	*
 *		the user to make a selection, or by using FileDialog-	*
 *		Popup() which will call a callback when a file is	*
 *		selected or the dialog is canceled.  The dialog can	*
 *		then be popped down using FileDialogPopdown().		*
 ************************************************************************/

FileDialog FileDialogCreate (parent, name)
    Widget parent;
    String name;
{
    Widget		group [4];
    FileDialog		filed;
    static XtAppContext	app_context = NULL;
    static XtActionsRec actions [ ] =
			{{"FileDialogOkay",   FileDialogOkay},
			 {"FileDialogCancel", FileDialogCancel}};


    /* Perform one time initialization. */

    if (app_context == NULL) {
	app_context = XtWidgetToApplicationContext (parent);
	XtAppAddActions (app_context, actions, XtNumber (actions));
	AddAutoRepeatAction (app_context);

	layout_args [0].value = StringToLayout (parent, layout_string);

	entry_translations    = XtParseTranslationTable (entry_table);
	viewport_translations = XtParseTranslationTable (viewport_table);
	list_translations     = XtParseTranslationTable (list_table);
	okay_translations     = XtParseTranslationTable (okay_table);
	cancel_translations   = XtParseTranslationTable (cancel_table);
    }


    /* Create the file dialog and its widgets. */

    filed = XtNew (struct file_dialog);

    filed -> path      = (String) XtMalloc (MaxPathLen);

    filed -> shell     = XtCreatePopupShell (name,
			 transientShellWidgetClass, parent,
			 NULL, 0);

    filed -> layout    = XtCreateManagedWidget ("layout",
			 layoutWidgetClass, filed -> shell,
			 layout_args, XtNumber (layout_args));

    filed -> label     = XtCreateManagedWidget ("label",
			 labelWidgetClass, filed -> layout,
			 label_args, XtNumber (label_args));

    filed -> directory = XtCreateManagedWidget ("directory",
			 labelWidgetClass, filed -> layout,
			 directory_args, XtNumber (directory_args));

    filed -> entry     = XtCreateManagedWidget ("entry",
			 asciiTextWidgetClass, filed -> layout,
			 entry_args, XtNumber (entry_args));

    filed -> viewport  = XtCreateManagedWidget ("viewport",
			 viewportWidgetClass, filed -> layout, 
			 viewport_args, XtNumber (viewport_args));

    filed -> list      = XtCreateManagedWidget ("list",
			 listWidgetClass, filed -> viewport,
			 list_args, XtNumber (list_args));

    filed -> okay      = XtCreateManagedWidget ("okay",
			 commandWidgetClass, filed -> layout,
			 NULL, 0);

    filed -> cancel    = XtCreateManagedWidget ("cancel",
			 commandWidgetClass, filed -> layout,
			 NULL, 0);


    /* Create a tab group for the file dialog. */

    group [0] = filed -> entry;
    group [1] = filed -> viewport;
    group [2] = filed -> okay;
    group [3] = filed -> cancel;

    XtGetValues (filed -> layout, color_args, XtNumber (color_args));
    CreateTabGroup (filed -> shell, group, XtNumber (group), highlight, True);
    XtRealizeWidget (filed -> shell);


    /* Add the translations to each widget. */

    AddDeleteWindowProtocol   (filed -> shell, "FileDialogCancel()");
    ListAddCursorTranslations (filed -> viewport);
    ListAddCursorAccelerators (filed -> viewport, filed -> entry);

    XtOverrideTranslations (filed -> entry,    entry_translations);
    XtOverrideTranslations (filed -> viewport, viewport_translations);
    XtOverrideTranslations (filed -> list,     list_translations);
    XtOverrideTranslations (filed -> okay,     okay_translations);
    XtOverrideTranslations (filed -> cancel,   cancel_translations);


    /* Add the necessary callbacks. */

    XtAddCallback (filed -> list, XtNcallback, CopySelected, (XtPointer) filed);
    XtAddCallback (filed -> cancel, XtNcallback, Cancel, (XtPointer) filed);
    XtAddCallback (filed -> okay, XtNcallback, Okay, (XtPointer) filed);

    return filed;
}


/************************************************************************
 * Function:	FileDialogSelect					*
 *									*
 * Description:	Pops up the file dialog with the specified shell title,	*
 *		label, and suggestion and waits until a selection is	*
 *		made or the dialog is canceled; answer will point to	*
 *		the selection or NULL if the dialog was canceled	*
 ************************************************************************/

void FileDialogSelect (filed, title, label, suggestion, answer)
    FileDialog filed;
    String     title;
    String     label;
    String     suggestion;
    String    *answer;
{
    XEvent       event;
    XtAppContext app_context;


    FileDialogPopup (filed, title, label, suggestion, NULL, NULL);

    filed -> status = Waiting;
    app_context = XtWidgetToApplicationContext (filed -> shell);

    while (filed -> status == Waiting) {
	XtAppNextEvent (app_context, &event);
	XtDispatchEvent (&event);
    }

    FileDialogPopdown (filed);

    if (answer != NULL)
	*answer = filed -> status == Canceled ? NULL : filed -> path;
}


/************************************************************************
 * Function:	FileDialogPopup						*
 *									*
 * Description: Pops up the file dialog with the specified shell title,	*
 *		label, and suggestion.  If the suggestion is NULL then	*
 *		the current working directory is used.  If the callback	*
 *		is not NULL then it will be called upon a selection or	*
 *		cancelation with the specified client data.		*
 ************************************************************************/

void FileDialogPopup (filed, title, label, suggestion, callback, client_data)
    FileDialog	   filed;
    String	   title;
    String	   label;
    String	   suggestion;
    XtCallbackProc callback;
    XtPointer	   client_data;
{
    Arg args [1];


    XtSetArg (args [0], XtNtitle, title);
    XtSetValues (filed -> shell, args, 1);

    XtSetArg (args [0], XtNlabel, label);
    XtSetValues (filed -> label, args, 1);

    if (suggestion == NULL)
	getcwd (filed -> path, MaxPathLen);
    else if (suggestion [0] == '/' || suggestion [0] == '~')
	strcpy (filed -> path, suggestion);
    else {
	getcwd (filed -> path, MaxPathLen);
	strcat (filed -> path, "/");
	strcat (filed -> path, suggestion);
    }

    XtSetArg (args [0], XtNstring, filed -> path);
    XtSetValues (filed -> entry, args, 1);

    filed -> callback = callback;
    filed -> client_data = client_data;

    SetFocus (filed -> entry);
    XtCallCallbacks (filed -> okay, XtNcallback, (XtPointer) filed);
    XtPopup (filed -> shell, callback == NULL ? XtGrabExclusive : XtGrabNone);
}


/************************************************************************
 * Function:	FileDialogPopdown					*
 *									*
 * Descripion:	Pops down the specified file dialog			*
 ************************************************************************/

void FileDialogPopdown (filed)
    FileDialog filed;
{
    XtPopdown (filed -> shell);
}
