/*
 * Copyright (c) 2005-2023 Julien Nadeau Carriere <vedge@csoft.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * File browser widget. It shows directories and shortcuts on the left and
 * those files contained in the selected directory on the right. It provides
 * a glob(3)-supporting textbox for manual input and a file-type selector.
 */

#include <agar/core/core.h>
#if defined(AG_WIDGETS)

#include <agar/core/config.h>

#include <agar/gui/file_dlg.h>
#include <agar/gui/box.h>
#include <agar/gui/numerical.h>
#include <agar/gui/checkbox.h>
#include <agar/gui/separator.h>
#include <agar/gui/icons.h>

#ifdef __NetBSD__
#define _NETBSD_SOURCE
#endif

#include <stdarg.h>
#include <string.h>
#include <ctype.h>

#ifdef _XBOX
# include <agar/core/xbox.h>
#elif _WIN32
# include <agar/core/win32.h>
#else
# include <sys/types.h>
# include <unistd.h>
# include <string.h>
# include <errno.h>
#endif

#include <agar/gui/file_dlg_common.h>

#include <agar/config/have_jpeg.h>
#include <agar/config/have_png.h>

static int  FileIsExecutable(AG_FileDlg *_Nonnull, char *_Nonnull);
static void RefreshListing(AG_FileDlg *_Nonnull);

/*
 * Set the container widget in which to display filetype-specific options.
 */
void
AG_FileDlgSetOptionContainer(AG_FileDlg *fd, void *ctr)
{
#ifdef AG_DEBUG
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	if (ctr) { AG_OBJECT_ISA(ctr, "AG_Widget:*"); }
#endif
	AG_ObjectLock(fd);
	fd->optsCtr = ctr;
	AG_ObjectUnlock(fd);
}

/*
 * Filter files by:
 *
 * - by filename extension:   ".txt"
 * - by executability:        "<-x>"
 * - exact filename match:    "<=hello.txt>"
 * - case-insensitive match:  "<=LICENSE/i>"
 * - TODO regular expression: "</image.(png|jpeg)/>"
 */
static int
FilterByExtension(AG_FileDlg *_Nonnull fd, char *_Nonnull file)
{
	char *ext, *s;
	AG_FileType *ft;
	Uint i;

	ext = strrchr(file, '.');

	TAILQ_FOREACH(ft, &fd->types, types) {
		for (i = 0; i < ft->nExts; i++) {
			const char *ftExt = ft->exts[i];

			if (ftExt[0] == '.' && ext &&
			    (s = strrchr(ftExt, '.')) != NULL &&
			    Strcasecmp(s, ext) == 0) {
				break;
			}
			if (strcmp(ftExt, "<-x>") == 0 &&
			    FileIsExecutable(fd, file)) {
				break;
			}
			/* Match "<=exact>" or "<=case-insensitive/i>" name */
			if (ftExt[0] == '<' && ftExt[1] == '=') {
				char pat[AG_FILENAME_MAX], *c;
				AG_Size patLen;
				
				Strlcpy(pat, &ftExt[2], sizeof(pat));
				if ((c = strrchr(pat, '>')) != NULL) {
					*c = '\0';
					patLen = strlen(pat);
					if (patLen > 2 &&
					    pat[patLen-1] == 'i' &&
					    pat[patLen-2] == '/') {
						pat[patLen-2] = '\0';
						if (Strcasecmp(file, pat) == 0)
							break;
					} else {
						if (strcmp(file, pat) == 0)
							break;
					}
				}
			}

			/* TODO: Regular expression */
		}
		if (i < ft->nExts)
			break;
	}
	return (ft == NULL);
}

/* Filter executable files (handle special "<-x>" attribute) */
static int
FileIsExecutable(AG_FileDlg *_Nonnull fd, char *_Nonnull file)
{
	char path[AG_PATHNAME_MAX];
	AG_FileInfo inf;

	Strlcpy(path, fd->cwd, sizeof(path));
	if (path[strlen(path) - 1] != AG_PATHSEPCHAR) {
		Strlcat(path, AG_PATHSEP, sizeof(path));
	}
	Strlcat(path, file, sizeof(path));

	AG_GetFileInfo(path, &inf);
	return (inf.perms & AG_FILE_EXECUTABLE);
}

/* File has been selected through COMPACT mode de-expansion. */
static void
CompactFileSelect(AG_Event *_Nonnull event)
{
	char path[AG_PATHNAME_MAX];
	AG_FileDlg *fdExpand = AG_FILEDLG_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);

	AG_FileDlgCopyFilename(fdExpand, path, sizeof(path));
	AG_TextboxSetString(fd->textbox, path);
	AG_TextboxSetCursorPos(fd->textbox, strlen(path));
	AG_WidgetFocus(fd->textbox);
	
	/* FileDlg will close the window for us (AG_FILEDLG_CLOSEWIN) */
	fd->winExpand = NULL;
	fd->fdExpand = NULL;
}

/* Collapse from COMPACT mode expansion. */
static void
CompactCollapse(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	
	fd->fdExpand = NULL;
	fd->winExpand = NULL;

	AG_ObjectDetach(win);
}

/* Expand a FileDlg in COMPACT mode */
static void
CompactExpand(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_Window *win = fd->winExpand;

	if (win) {
		AG_ObjectDetach(win);
		fd->winExpand = NULL;
		fd->fdExpand = NULL;
	}
	if ((fd->winExpand = win = AG_WindowNew(0)) == NULL)
		return;

	AG_WindowSetCaption(win, _("Please select a file"));

	fd->fdExpand = AG_FileDlgNew(win,
	    (fd->flags & AG_FILEDLG_INHERITED_FLAGS) |
	    (AG_FILEDLG_CLOSEWIN | AG_FILEDLG_EXPAND));

	AG_FileDlgCopyTypes(fd->fdExpand, fd);

	AG_SetEvent(fd->fdExpand, "file-chosen", CompactFileSelect, "%p", fd);
	AG_SetEvent(win, "window-close", CompactCollapse, "%p", fd);
	AG_WindowShow(win);
}

/*
 * Filesystem event callback. We simply re-open the directory and refresh.
 */
static int
OnDirectoryEvent(AG_EventSink *_Nonnull es, AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);

	if (fd->fdDir != -1) {
		AG_Debug(fd, "Directory event (dir = %d)\n", fd->fdDir);
		if (fd->esFollow) {
			AG_DelEventSink(fd->esFollow);
			fd->esFollow = NULL;
		}
		fd->fdDir = -1;

		AG_CloseDir(fd->openDir);
		fd->openDir = NULL;
	}
	RefreshListing(fd);
	return (0);
}

/* Update the file / directory listing */
static void
RefreshListing(AG_FileDlg *_Nonnull fd)
{
	AG_Tlist *tlDirs = fd->tlDirs;
	AG_Tlist *tlFiles = fd->tlFiles;
	AG_TlistItem *it;
	AG_FileInfo info;
	AG_Dir *dir;
	char **dirs, **files;
	Uint i, nDirs=0, nFiles=0;

	if (tlDirs == NULL /* || tlFiles == NULL */) {
		return;
	}
	if (fd->openDir != NULL) {
		AG_CloseDir(fd->openDir);
	}
	if ((dir = fd->openDir = AG_OpenDir(fd->cwd)) == NULL) {
		AG_TextMsg(AG_MSG_ERROR, "%s: %s", fd->cwd, AG_GetError());
		return;
	}
	if (dir->fd != -1) {
		fd->fdDir = dir->fd;
		/*
		 * Follow filesystem events related to the directory.
		 */
		if (fd->esFollow) {
			AG_DelEventSink(fd->esFollow);
		}
		fd->esFollow = AG_AddEventSink(AG_SINK_FSEVENT, dir->fd,
		    (AG_FSEVENT_WRITE | AG_FSEVENT_DELETE | AG_FSEVENT_LINK |
		     AG_FSEVENT_RENAME | AG_FSEVENT_REVOKE),
		    OnDirectoryEvent, "%p", fd);

		AG_Debug(fd, "Following %s (es=%p)\n", fd->cwd, fd->esFollow);
	}
	
	dirs = Malloc(sizeof(char *));
	files = Malloc(sizeof(char *));
	
	AG_ObjectLock(tlDirs);
	AG_ObjectLock(tlFiles);

	for (i = 0; i < dir->nents; i++) {
		char *ent = dir->ents[i];
		char path[AG_FILENAME_MAX];
		
		Strlcpy(path, fd->cwd, sizeof(path));
		if (path[strlen(path) - 1] != AG_PATHSEPCHAR) {
			Strlcat(path, AG_PATHSEP, sizeof(path));
		}
		Strlcat(path, ent, sizeof(path));

		if (AG_PathIsFilesystemRoot(fd->cwd) &&
		    strcmp(ent, "..")==0) {
			continue;
		}
		if (AG_GetFileInfo(path, &info) == -1) {
			continue;
		}
		/* XXX TODO: check for symlinks to directories */
		if (info.type == AG_FILE_DIRECTORY) {
			dirs = Realloc(dirs, (nDirs + 1) * sizeof(char *));
			dirs[nDirs++] = Strdup(ent);
		} else {
			AG_Size entLen;
			char *s;

			if (fd->flags & AG_FILEDLG_MASK_HIDDEN &&
			    ent[0] == '.') {
				continue;
			}
			if (fd->flags & AG_FILEDLG_MASK_EXT &&
			    FilterByExtension(fd, ent)) {
				continue;
			}
			files = Realloc(files, (nFiles + 1) * sizeof(char *));
			entLen = strlen(ent);
			s = files[nFiles] = Malloc(entLen + 2);
			memcpy(s, ent, entLen);

			if (info.perms & AG_FILE_EXECUTABLE) {
				s[entLen] = 'x';
			} else if ((info.perms & AG_FILE_READABLE) == 0) {
				s[entLen] = 'R';
			} else if (ent[0] == '.') {
				s[entLen] = 'r';
			} else {
				s[entLen] = '?';
			}
			s[entLen+1] = '\0';
			nFiles++;
		}
	}

	qsort(dirs, nDirs, sizeof(char *), AG_FilenameCompare);
	qsort(files, nFiles, sizeof(char *), AG_FilenameCompare);

	AG_TlistClear(tlDirs);
	AG_TlistClear(tlFiles);

	for (i = 0; i < nDirs; i++) {
		char *s = dirs[i];

		it = AG_TlistAddS(tlDirs, agIconDirectory.s, s);
		it->cat = "dir";
		it->p1 = it;
		free(s);
	}
	for (i = 0; i < nFiles; i++) {
		char *s = files[i];
		char *pAttr = &s[strlen(s)-1], attr = *pAttr;

		*pAttr = '\0';
		it = AG_TlistAddS(tlFiles, agIconDoc.s, s);
		it->cat = "file";
		it->p1 = it;
		switch (attr) {
		case 'x':
			/* Display executables in bold. */
			AG_TlistSetFont(tlFiles, it, NULL, 1.0f, AG_FONT_BOLD);
			break;
		case 'R':
		case 'r':
			/* Display unreadable and normally hidden files in italic. */
			AG_TlistSetFont(tlFiles, it, NULL, 1.0f, AG_FONT_ITALIC);
			break;
		}
		free(s);
	}

	free(dirs);
	free(files);
	AG_TlistRestore(tlDirs);
	AG_TlistRestore(tlFiles);

	AG_TlistScrollToStart(tlFiles);
	
	AG_ObjectUnlock(tlFiles);
	AG_ObjectUnlock(tlDirs);
}

void
AG_FileDlgRefresh(AG_FileDlg *fd)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	RefreshListing(fd);
}

static void
DirSelected(AG_Event *_Nonnull event)
{
	AG_Tlist *tl = AG_TLIST_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_TlistItem *ti;

	AG_ObjectLock(fd);
	AG_ObjectLock(tl);

	if ((ti = AG_TlistSelectedItem(tl)) != NULL) {
		if (AG_FileDlgSetDirectoryS(fd, ti->text) == -1) {
			/* AG_TextMsgFromError() */
		} else {
			AG_PostEvent(fd, "dir-selected", "%s", fd->cwd);
			RefreshListing(fd);
		}
	}

	AG_ObjectUnlock(tl);
	AG_ObjectUnlock(fd);
}

/* Select a directory shortcut. */
static void
LocSelected(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	const AG_TlistItem *ti = AG_TLISTITEM_PTR(2);

	if (ti == NULL) {
		return;
	}
	if (AG_FileDlgSetDirectoryS(fd, ti->text) == -1) {
		/* AG_TextMsgFromError() */
	} else {
		AG_PostEvent(fd, "dir-selected", "%s", fd->cwd);
		RefreshListing(fd);
	}
}

/* Expand directory shortcuts. */
static void
LocExpanded(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_Combo *comLoc = fd->comLoc;
	AG_Tlist *tl = comLoc->list;

	AG_TlistSetCompareFn(tl, AG_TlistCompareStrings);

#ifdef _WIN32
	{
		char path[4];
		int drive;
# ifdef _XBOX
		DWORD d = AG_XBOX_GetLogicalDrives();
# else
		DWORD d = GetLogicalDrives();
# endif
		/* Add the Windows drives */
		for (drive = 0; drive < 26; drive++) {
			AG_TlistItem *ti;

			if (!(d & (1 << drive))) {
				continue;
			}
			path[0] = 'A'+drive;
			path[1] = ':';
			path[2] = '\\';
			path[3] = '\0';
			ti = AG_TlistAddS(tl, agIconDirectory.s, path);

			if (toupper(fd->cwd[0]) == path[0] &&
			    fd->cwd[1] == ':') {
				AG_ComboSelect(comLoc, ti);
			}
#if 0
			/* TODO icons, etc */
			switch (GetDriveType(path)) {
			case DRIVE_UNKNOWN:
			case DRIVE_NO_ROOT_DIR:
			case DRIVE_REMOVABLE:
			case DRIVE_FIXED:
			case DRIVE_REMOTE:
			case DRIVE_CDROM:
			case DRIVE_RAMDISK:
				break;
			}
#endif
		}
	}
#else /* !_WIN32 */
	{
		char path[AG_PATHNAME_MAX];
		AG_ConfigPath *loadPath;
		AG_User *sysUser;
	
		AG_TlistAddS(tl, agIconDirectory.s, "/");

		if ((sysUser = AG_GetRealUser()) != NULL) {
			AG_TlistAddS(tl, agIconDirectory.s, sysUser->home);
			AG_UserFree(sysUser);
		}
		if (AG_GetCWD(path, sizeof(path)) == 0) {
			AG_TlistAddS(tl, agIconDirectory.s, path);
		}
		if (fd->flags & AG_FILEDLG_SAVE) {
			if (AG_ConfigGetPath(AG_CONFIG_PATH_DATA, 0,
			    path, sizeof(path)) < sizeof(path))
				AG_TlistAddS(tl, agIconDirectory.s, path);
		} else {
			AG_ConfigPathQ *pathGroup =
			    &agConfig->paths[AG_CONFIG_PATH_DATA];

			TAILQ_FOREACH(loadPath, pathGroup, paths)
				AG_TlistAddS(tl, agIconDirectory.s, loadPath->s);
		}
		AG_ComboSelectText(comLoc, fd->cwd);
	}
	AG_TlistUniq(tl);
#endif /* _WIN32 */
}

static void
ChooseFile(AG_FileDlg *_Nonnull fd, AG_Window *_Nonnull winParent)
{
	AG_FileType *ft;
	char *ext;
	int i;

	AG_ObjectLock(fd);

	if (fd->curType != NULL) {                         /* Selected type */
		ft = fd->curType;
	} else if ((ext = strrchr(fd->cfile, '.')) != NULL) {
		TAILQ_FOREACH(ft, &fd->types, types) {        /* Infer type */
			for (i = 0; i < ft->nExts; i++) {
				char *s;
				if ((s = strrchr(ft->exts[i], '.')) != NULL &&
				    Strcasecmp(s, ext) == 0)
					break;
			}
			if (i < ft->nExts)
				break;
		}
	} else {
		ft = NULL;
	}
	if (ft != NULL && ft->action) {
		AG_PostEventByPtr(fd, ft->action, "%s,%p", fd->cfile, ft);
	}
	AG_PostEvent(fd, "file-chosen", "%s,%p", fd->cfile, ft);

	if (fd->flags & AG_FILEDLG_CLOSEWIN) {
/*		AG_PostEvent(winParent, "window-close", NULL); */
		AG_ObjectDetach(winParent);
	}
	AG_ObjectUnlock(fd);
}

static void
ReplaceFileConfirm(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_Window *qwin = AG_WINDOW_PTR(2);
	AG_Window *winParent = AG_WINDOW_PTR(3);

	ChooseFile(fd, winParent);
	AG_ObjectDetach(qwin);
}

static void
ReplaceFileDlg(AG_FileDlg *_Nonnull fd, AG_Window *_Nonnull winParent)
{
	AG_Window *win;
	AG_Button *btn;
	AG_Box *hb;

	win = AG_WindowNew(AG_WINDOW_NORESIZE | AG_WINDOW_NOTITLE);
	AG_WindowSetPosition(win, AG_WINDOW_CENTER, 0);

	AG_LabelNew(win, 0, _("File %s exists. Overwrite?"), fd->cfile);

	hb = AG_BoxNewHoriz(win, AG_BOX_HOMOGENOUS | AG_BOX_HFILL);
	{
		AG_ButtonNewFn(hb, 0, _("Yes"),
		    ReplaceFileConfirm, "%p,%p,%p", fd, win, winParent);
		btn = AG_ButtonNewFn(hb, 0, _("Cancel"), AGWINDETACH(win));
		AG_WidgetFocus(btn);
	}
//	AG_WindowAttach(winParent, win);
	AG_WindowShow(win);
}

int
AG_FileDlgCheckReadAccess(AG_FileDlg *fd)
{
	AG_FileInfo info;

	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);

	if (AG_GetFileInfo(fd->cfile, &info) == -1) {
		goto fail;
	}
	if ((info.perms & AG_FILE_READABLE) == 0) {
		AG_SetError(_("%s: Read permission denied"), fd->cfile);
		goto fail;
	}

	AG_ObjectUnlock(fd);
	return (0);
fail:
	AG_ObjectUnlock(fd);
	return (-1);
}

int
AG_FileDlgCheckWriteAccess(AG_FileDlg *fd)
{
	AG_FileInfo info;
	
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);

	if (AG_GetFileInfo(fd->cfile, &info) == -1) {
		goto fail;
	}
	if ((info.perms & AG_FILE_WRITEABLE) == 0) {
		AG_SetError(_("%s: Write permission denied"), fd->cfile);
		goto fail;
	}

	AG_ObjectUnlock(fd);
	return (0);
fail:
	AG_ObjectUnlock(fd);
	return (-1);
}

static void
CheckAccessAndChoose(AG_FileDlg *_Nonnull fd)
{
	AG_Window *winParent = AG_ParentWindow(fd);
	char *s;

	for (s = &fd->cfile[0]; *s != '\0'; s++) {
		if (!isspace((int) *s))
			break;
	}
	if (*s == '\0')
		return;

	if (fd->flags & AG_FILEDLG_LOAD) {
		if (AG_FileDlgCheckReadAccess(fd) == -1) {
			AG_TextMsgFromError();
			return;
		}
	} else if (fd->flags & AG_FILEDLG_SAVE) {
		AG_FileInfo info;

		/*
		 * Display a "replace file?" dialog if this file
		 * already exists.
		 */
		if (AG_GetFileInfo(fd->cfile, &info) == 0) {
			if (info.perms & AG_FILE_WRITEABLE) {
				ReplaceFileDlg(fd, winParent);
			} else {
				AG_TextMsg(AG_MSG_ERROR,
				    _("%s: File exists and is non-writeable"),
				    fd->cfile);
			}
			return;
		}
	}
	ChooseFile(fd, winParent);
}

static void
FileSelected(AG_Event *_Nonnull event)
{
	AG_Tlist *tl = AG_TLIST_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_FileType *ft;
	AG_TlistItem *ti;
	char *ext;

	AG_ObjectLock(fd);

	if ((ti = AG_TlistSelectedItem(tl)) != NULL) {
		AG_FileDlgSetFilenameS(fd, ti->text);
		AG_PostEvent(fd, "file-selected", "%s", fd->cfile);
	}

	Debug(fd, "Selected file \"%s\"\n", fd->cfile);

	if ((ext = strrchr(fd->cfile, '.')) == NULL) {
		/* TODO */
		goto out;
	}
	TAILQ_FOREACH(ft, &fd->types, types) {
		Uint i;

		for (i = 0; i < ft->nExts; i++) {
			char *ftExt;

			if ((ftExt = strrchr(ft->exts[i], '.')) == NULL) {
				continue;
			}
			if (Strcasecmp(ftExt, ext) == 0)
				break;
		}
		if (i < ft->nExts) {
			Debug(fd, "File matches <%s>\n", ft->descr);
			AG_FileDlgSelectType(fd, ft);
			break;
		}
	}
out:
	AG_ObjectUnlock(fd);
}

static void
FileDblClicked(AG_Event *_Nonnull event)
{
	AG_Tlist *tl = AG_TLIST_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_TlistItem *itFile;

	AG_ObjectLock(fd);
	AG_ObjectLock(tl);

	if ((itFile = AG_TlistSelectedItem(tl)) != NULL) {
		AG_FileDlgSetFilenameS(fd, itFile->text);

		if (fd->okAction) {
			AG_PostEventByPtr(fd, fd->okAction, "%s", fd->cfile);
		} else {
			CheckAccessAndChoose(fd);
		}
	}

	AG_ObjectUnlock(tl);
	AG_ObjectUnlock(fd);
}

static void
PressedOK(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);

	AG_ObjectLock(fd);

	if (fd->okAction) {
		AG_PostEventByPtr(fd, fd->okAction, "%s", fd->cfile);
	} else {
		CheckAccessAndChoose(fd);
	}

	AG_ObjectUnlock(fd);
}

static void
SetFilename(AG_FileDlg *_Nonnull fd, const char *_Nonnull file)
{
	if (file[0] == AG_PATHSEPCHAR) {
		Strlcpy(fd->cfile, file, sizeof(fd->cfile));
	} else {
		Strlcpy(fd->cfile, fd->cwd, sizeof(fd->cfile));
		if (!AG_PathIsFilesystemRoot(fd->cwd) &&
		    (fd->cfile[0] != '\0' &&
		     fd->cfile[strlen(fd->cfile)-1] != AG_PATHSEPCHAR)) {
			Strlcat(fd->cfile, AG_PATHSEP, sizeof(fd->cfile));
		}
		Strlcat(fd->cfile, file, sizeof(fd->cfile));
	}
}

static void
TextboxChanged(AG_Event *_Nonnull event)
{
	char path[AG_PATHNAME_MAX];
	AG_Textbox *tb = AG_TEXTBOX_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);

	AG_ObjectLock(fd);

	AG_TextboxCopyString(tb, path, sizeof(path));
	SetFilename(fd, path);

	AG_ObjectUnlock(fd);
}

#ifdef HAVE_GLOB
static void
SelectGlobResult(AG_Event *_Nonnull event)
{
	char file[AG_PATHNAME_MAX];
	AG_Window *win = AG_WINDOW_PTR(1);
	AG_FileDlg *fd = AG_FILEDLG_PTR(2);
	const AG_TlistItem *ti = AG_TLISTITEM_PTR(3);
	AG_Textbox *tb = fd->tbFile;
	AG_FileInfo info;
	int endSep;

	AG_ObjectLock(fd);

	Strlcpy(file, ti->text, sizeof(file));
	endSep = (file[strlen(file)-1]==AG_PATHSEPCHAR) ? 1 : 0;
	AG_TextboxSetString(tb, file);

	if (endSep ||
	    (AG_GetFileInfo(file,&info)==0 && info.type == AG_FILE_DIRECTORY)) {
		if (AG_FileDlgSetDirectoryS(fd, file) == 0) {
			RefreshListing(fd);
		} else {
			/* AG_TextMsgFromError() */
			goto out;
		}
	} else {
		AG_FileDlgSetFilenameS(fd, file);
		CheckAccessAndChoose(fd);
	}
out:
	AG_ObjectUnlock(fd);
	AG_ObjectDetach(win);
}

static void
CloseGlobResults(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_PTR(1);

	AG_ObjectDetach(win);
}

static void
ExpandGlobResults(AG_FileDlg *_Nonnull fd, glob_t *_Nonnull gl,
    const char *_Nonnull pattern)
{
	AG_Window *winParent = WIDGET(fd)->window;
	AG_Window *win;
	Uint wView, hView;
	int x, y, w, h;
	int wMax = 0, hMax = 0;
	AG_Button *btn;
	AG_Tlist *tl;
	int i;
	
	if ((win = AG_WindowNew(0)) == NULL) {
		return;
	}
	AG_WindowAttach(winParent, win);
	AG_WindowSetCaption(win, _("Matching \"%s\""), pattern);

	tl = AG_TlistNew(win, AG_TLIST_EXPAND);
	AG_SetEvent(tl, "tlist-selected", SelectGlobResult, "%p,%p", win, fd);
	btn = AG_ButtonNewFn(win, AG_BUTTON_HFILL, _("Dismiss"),
	    CloseGlobResults, "%p", win);
	AG_WidgetFocus(btn);

	for (i = 0; i < gl->gl_pathc; i++) {
		char *p = gl->gl_pathv[i];
		AG_FileInfo fi;
		AG_TlistItem *ti;

		if (AG_GetFileInfo(p, &fi) != 0) {
			continue;
		}
		if (fi.type == AG_FILE_DIRECTORY) {
			if (p[strlen(p)-1] != AG_PATHSEPCHAR) {
				ti = AG_TlistAdd(tl, agIconDirectory.s,
				    "%s%c", p, AG_PATHSEPCHAR);
			} else {
				ti = AG_TlistAddS(tl, agIconDirectory.s, p);
			}
		} else {
			ti = AG_TlistAddS(tl, agIconDoc.s, p);
		}
		ti->p1 = &gl->gl_pathv[i];
		AG_TextSize(p, &w, NULL);
		if (w > wMax) { wMax = w; }
		hMax++;
	}

	/* Compute geometry. */
	w = wMax + 100 + tl->item_h+2 +
	    agTextFontHeight; /* scrollbar size */
	h = hMax*tl->item_h + 32;
	x = WIDGET(fd->tbFile)->rView.x2 - w;
	y = WIDGET(fd->tbFile)->rView.y1;
	if (AGDRIVER_MULTIPLE(WIDGET(fd)->drv) &&
	    winParent) {
		x += WIDGET(winParent)->x;
		y += WIDGET(winParent)->y;
	}

	/* Limit to display area. */
	AG_GetDisplaySize(WIDGET(fd)->drv, &wView, &hView);
	if (x < 0) { x = 0; }
	if (y < 0) { y = 0; }
	if (x+w > wView) { w = wView - x; }
	if (y+h > hView) { h = hView - y; }
	if (w < 5 || h < 5) {
		AG_ObjectDetach(win);
		return;
	}

	AG_WindowSetGeometry(win, x, y, w, h);
	AG_WindowShow(win);
}

static int
GlobExpansion(AG_FileDlg *_Nonnull fd, char *_Nonnull path, AG_Size pathLen)
{
	char *pathOrig;
	glob_t gl;
	int globFlags = 0;

#if defined(GLOB_TILDE)
	globFlags |= GLOB_TILDE;
#endif
	if ((pathOrig = TryStrdup(path)) == NULL) {
		return (0);
	}
	if (glob(path, globFlags, NULL, &gl) != 0) {
		goto out;
	}
	if (gl.gl_pathc == 1) {
		Strlcpy(path, gl.gl_pathv[0], pathLen);
	} else if (gl.gl_pathc > 1) {
		ExpandGlobResults(fd, &gl, pathOrig);
		free(pathOrig);
		return (1);
	}
	globfree(&gl);
out:
	free(pathOrig);
	return (0);
}
#endif /* HAVE_GLOB */

static void
TextboxReturn(AG_Event *_Nonnull event)
{
	char file[AG_PATHNAME_MAX];
	AG_Textbox *tb = AG_TEXTBOX_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_FileInfo info;
	int endSep;
	
	AG_ObjectLock(fd);

	AG_TextboxCopyString(tb, file, sizeof(file));
#ifdef HAVE_GLOB
	if (GlobExpansion(fd, file, sizeof(file)))
		goto out;
#endif
	if (file[0] == '\0') {
		goto out;
	}
	endSep = (file[strlen(file)-1]==AG_PATHSEPCHAR) ? 1 : 0;
	if (ProcessFilename(file, sizeof(file)) == -1) {
		goto out;
	}
	AG_TextboxSetString(tb, file);

	if (endSep ||
	    (AG_GetFileInfo(file,&info)==0 && info.type == AG_FILE_DIRECTORY)) {
		if (AG_FileDlgSetDirectoryS(fd, file) == 0) {
			RefreshListing(fd);
		} else {
			/* AG_TextMsgFromError() */
			goto out;
		}
	} else {
		AG_FileDlgSetFilenameS(fd, file);
		CheckAccessAndChoose(fd);
	}
out:
	AG_ObjectUnlock(fd);
}

static void
PressedCancel(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_Window *winParent;

	AG_ObjectLock(fd);

	if (fd->cancelAction) {
		AG_PostEventByPtr(fd, fd->cancelAction, NULL);
	} else if (fd->flags & AG_FILEDLG_CLOSEWIN) {
		if ((winParent = AG_ParentWindow(fd)) != NULL) {
			AG_PostEvent(winParent, "window-close", NULL);
/*			AG_ObjectDetach(winParent); */
		}
	}

	AG_ObjectUnlock(fd);
}

/*
 * Select the active file type. If there are file-type specific options,
 * create any necessary widgets to edit them.
 */
void
AG_FileDlgSelectType(AG_FileDlg *fd, AG_FileType *ft)
{
	AG_FileOption *fo;
	AG_Numerical *num;
	AG_Textbox *tbox;

	fd->curType = ft;

	if (fd->optsCtr == NULL) {
		return;
	}
	AG_ObjectFreeChildren(fd->optsCtr);
	AG_WindowUpdate(WIDGET(fd->optsCtr)->window);

	if (ft == NULL)
		return;

	TAILQ_FOREACH(fo, &ft->opts, opts) {
		switch (fo->type) {
		case AG_FILEDLG_BOOL:
			AG_CheckboxNewInt(fd->optsCtr, AG_CHECKBOX_EXCL,
			    fo->descr,
			    &fo->data.i.val);
			break;
		case AG_FILEDLG_INT:
			num = AG_NumericalNewS(fd->optsCtr, AG_NUMERICAL_HFILL,
			    NULL, fo->descr);
			AG_BindInt(num, "value", &fo->data.i.val);
			AG_BindInt(num, "min", &fo->data.i.min);
			AG_BindInt(num, "max", &fo->data.i.max);
			break;
		case AG_FILEDLG_FLOAT:
			num = AG_NumericalNewS(fd->optsCtr, AG_NUMERICAL_HFILL,
			    fo->unit, fo->descr);
			AG_BindFloat(num, "value", &fo->data.flt.val);
			AG_BindFloat(num, "min", &fo->data.flt.min);
			AG_BindFloat(num, "max", &fo->data.flt.max);
			break;
		case AG_FILEDLG_DOUBLE:
			num = AG_NumericalNewS(fd->optsCtr, AG_NUMERICAL_HFILL,
			    fo->unit, fo->descr);
			AG_BindDouble(num, "value", &fo->data.dbl.val);
			AG_BindDouble(num, "min", &fo->data.dbl.min);
			AG_BindDouble(num, "max", &fo->data.dbl.max);
			break;
		case AG_FILEDLG_STRING:
			tbox = AG_TextboxNewS(fd->optsCtr,
			    AG_TEXTBOX_EXCL | AG_TEXTBOX_HFILL,
			    fo->descr);
#ifdef AG_UNICODE
			AG_TextboxBindUTF8(tbox, fo->data.s, sizeof(fo->data.s));
#else
			AG_TextboxBindASCII(tbox, fo->data.s, sizeof(fo->data.s));
#endif
			break;
		}
	}
	AG_SetFontSize(fd->optsCtr, "90%");

	WIDGET(fd)->flags |= AG_WIDGET_UPDATE_WINDOW;
	AG_Redraw(fd);
}

/* A file type has been selected by the user. */
static void
FileTypeSelected(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	const AG_TlistItem *it = AG_TLISTITEM_PTR(2);

	AG_ObjectLock(fd);

	if (it != NULL) {
		AG_FileDlgSelectType(fd, it->p1);
	} else {
		AG_FileDlgSelectType(fd, TAILQ_FIRST(&fd->types));
	}

	AG_ObjectUnlock(fd);
}

/* The file types have been expanded by the user. */
static void
FileTypesExpanded(AG_Event *_Nonnull event)
{
	AG_Combo *com = AG_COMBO_SELF();
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);
	AG_Tlist *tl = com->list;
	AG_FileType *ft;
	AG_TlistItem *it;

	it = AG_TlistAddS(tl, NULL, _("Any file ( *.* )"));
	it->p1 = NULL;

	if (fd->curType == NULL)
		it->selected = 1;

	TAILQ_FOREACH(ft, &fd->types, types) {
		char extsLbl[64];
		Uint i;
	
		extsLbl[0] = '\0';
		for (i = 0; i < ft->nExts; i++) {
			const char *ext = ft->exts[i];

			if (strcmp("<-x>", ext) == 0) {
				Strlcat(extsLbl, "* ", sizeof(extsLbl));
			} else if (ext[0] == '<' && ext[1] == '=') {
				char *extDup = Strdup(ext);

				extDup[strlen(ext) - 1] = '\0';
				Strlcat(extsLbl, &extDup[2], sizeof(extsLbl));
				Strlcat(extsLbl, " ", sizeof(extsLbl));
				free(extDup);
			} else {
				Strlcat(extsLbl, "*", sizeof(extsLbl));
				Strlcat(extsLbl, ext, sizeof(extsLbl));
				Strlcat(extsLbl, " ", sizeof(extsLbl));
			}
		}
		if (extsLbl[0] != '\0') {
			it = AG_TlistAdd(tl, NULL, "%s ( %s)", ft->descr, extsLbl);
		} else {
			it = AG_TlistAddS(tl, NULL, ft->descr);
		}
		it->p1 = ft;
	}
}

static void
OnShow(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_SELF();

	if (!(fd->flags & AG_FILEDLG_RESET_ONSHOW)) {
		return;
	}
	fd->flags &= ~(AG_FILEDLG_RESET_ONSHOW);

	if (fd->tbFile) {
		AG_WidgetFocus(fd->tbFile);
	}
	RefreshListing(fd);
}

static void
OnHide(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_SELF();

	if (fd->esFollow) {
		AG_Debug(fd, "Unfollowing %s (es=%p)\n", fd->cwd, fd->esFollow);
		AG_DelEventSink(fd->esFollow);
		fd->esFollow = NULL;
	}
}

/* Get an auto-allocated copy of the current filename. */
char *
AG_FileDlgGetFilename(AG_FileDlg *fd)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	return Strdup(fd->cfile);
}

/* Get an auto-allocated copy of the working directory. */
char *
AG_FileDlgGetDirectory(AG_FileDlg *fd)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	return Strdup(fd->cwd);
}

/* Copy the current filename into a fixed-size buffer. */
AG_Size
AG_FileDlgCopyFilename(AG_FileDlg *fd, char *dst, AG_Size dstSize)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	return AG_Strlcpy(dst, fd->cfile, dstSize);
}

/* Copy the working directory into a fixed-size buffer. */
AG_Size
AG_FileDlgCopyDirectory(AG_FileDlg *fd, char *dst, AG_Size dstSize)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	return AG_Strlcpy(dst, fd->cwd, dstSize);
}

/* Move to the specified directory (format string). */
int
AG_FileDlgSetDirectory(AG_FileDlg *fd, const char *fmt, ...)
{
	char path[AG_PATHNAME_MAX];
	va_list ap;

	va_start(ap, fmt);
	Vsnprintf(path, sizeof(path), fmt, ap);
	va_end(ap);
	
	return AG_FileDlgSetDirectoryS(fd, path);
}

/* Move to the specified directory (C string). */
int
AG_FileDlgSetDirectoryS(AG_FileDlg *fd, const char *dir)
{
	AG_FileInfo info;
	char ncwd[AG_PATHNAME_MAX], *c;
	
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);

	if (dir[0] == '.' && dir[1] == '\0') {
		Strlcpy(ncwd, fd->cwd, sizeof(ncwd));
	} else if (dir[0] == '.' && dir[1] == '.' && dir[2] == '\0') {
		if (!AG_PathIsFilesystemRoot(fd->cwd)) {
			Strlcpy(ncwd, fd->cwd, sizeof(ncwd));
			if ((c = strrchr(ncwd, AG_PATHSEPCHAR)) != NULL) {
				*c = '\0';
			}
			if (c == &ncwd[0]) {
				ncwd[0] = AG_PATHSEPCHAR;
				ncwd[1] = '\0';
			}
#ifdef _XBOX
			if (AG_PathIsFilesystemRoot(ncwd) &&
			    ncwd[2] != AG_PATHSEPCHAR) {
				Strlcat(ncwd, AG_PATHSEP, sizeof(ncwd));
			}
#endif
		}
	} else if (!AG_PathIsAbsolute(dir)) {
		Strlcpy(ncwd, fd->cwd, sizeof(ncwd));
		if (!(ncwd[0] == AG_PATHSEPCHAR &&
		      ncwd[1] == '\0') &&
			  ncwd[strlen(ncwd) - 1] != AG_PATHSEPCHAR) {
			Strlcat(ncwd, AG_PATHSEP, sizeof(ncwd));
		}
		Strlcat(ncwd, dir, sizeof(ncwd));
	} else {
		Strlcpy(ncwd, dir, sizeof(ncwd));
	}

	if (AG_GetFileInfo(ncwd, &info) == -1) {
		goto fail;
	}
	if (info.type != AG_FILE_DIRECTORY) {
		AG_SetError(_("%s: Not a directory"), ncwd);
		goto fail;
	}
	if ((info.perms & (AG_FILE_READABLE | AG_FILE_EXECUTABLE)) == 0) {
		AG_SetError(_("%s: Permission denied"), ncwd);
		goto fail;
	}
	if (Strlcpy(fd->cwd, ncwd, sizeof(fd->cwd)) >= sizeof(fd->cwd)) {
		AG_SetError(_("Path is too long: `%s'"), ncwd);
		goto fail;
	}
	if (fd->dirMRU) {
		AG_SetString(agConfig, fd->dirMRU, fd->cwd);
		AG_ConfigSave();
	}

	if (fd->tlDirs) { AG_TlistScrollToStart(fd->tlDirs); }
	if (fd->tlFiles) { AG_TlistScrollToStart(fd->tlFiles); }

	AG_ObjectUnlock(fd);
	return (0);
fail:
	AG_ObjectUnlock(fd);
	return (-1);
}

/* Set the current directory (fetch default from specified MRU). */
void
AG_FileDlgSetDirectoryMRU(AG_FileDlg *fd, const char *key, const char *dflt)
{
	AG_Config *cfg = agConfig;
	char *s;

	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);
	AG_ObjectLock(cfg);

	if (AG_Defined(cfg,key) && (s = AG_GetStringDup(cfg,key))) {
		AG_FileDlgSetDirectoryS(fd, s);
		Free(s);
	} else {
		AG_SetString(cfg, key, dflt);
		if (AG_ConfigSave() == -1) {
			Verbose("Saving MRU(%s): %s\n", key, AG_GetError());
		}
		AG_FileDlgSetDirectoryS(fd, dflt);
	}
	Free(fd->dirMRU);
	fd->dirMRU = Strdup(key);
	
	AG_ObjectUnlock(cfg);
	AG_ObjectUnlock(fd);
}

/* Set the current filename (format string). */
void
AG_FileDlgSetFilename(AG_FileDlg *fd, const char *fmt, ...)
{
	char file[AG_FILENAME_MAX];
	va_list ap;
	
	va_start(ap, fmt);
	Vsnprintf(file, sizeof(file), fmt, ap);
	va_end(ap);

	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);

	SetFilename(fd, file);
	if (fd->tbFile) {
		AG_TextboxSetString(fd->tbFile, file);
/*		AG_TextboxSetCursorPos(fd->tbFile, -1); */
	}

	AG_ObjectUnlock(fd);
}

/* Set the current filename (C string). */
void
AG_FileDlgSetFilenameS(AG_FileDlg *fd, const char *s)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);

	SetFilename(fd, s);
	if (fd->tbFile) {
		AG_TextboxSetString(fd->tbFile, s);
/*		AG_TextboxSetCursorPos(fd->tbFile, -1); */
	}

	AG_ObjectUnlock(fd);
}

static void
MaskOptionSelected(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_PTR(1);

	RefreshListing(fd);
}

AG_FileDlg *
AG_FileDlgNewMRU(void *parent, const char *mruKey, Uint flags)
{
	char path[AG_PATHNAME_MAX];
	AG_FileDlg *fd;

	fd = AG_FileDlgNew(parent, flags);

	if (AG_ConfigGetPath(AG_CONFIG_PATH_DATA, 0, path, sizeof(path))
	    < sizeof(path)) {
		AG_FileDlgSetDirectoryMRU(fd, mruKey, path);
	}
	return (fd);
}

AG_FileDlg *
AG_FileDlgNewCompact(void *parent, const char *label, Uint flags)
{
	AG_FileDlg *fd;

	fd = AG_FileDlgNew(parent, (flags | AG_FILEDLG_COMPACT));
	if (label) {
		AG_TextboxSetLabelS(fd->textbox, label);
	}
	return (fd);
}

AG_FileDlg *
AG_FileDlgNewCompactMRU(void *parent, const char *mruKey, const char *label,
    Uint flags)
{
	char path[AG_PATHNAME_MAX];
	AG_FileDlg *fd;

	fd = AG_FileDlgNew(parent, (flags | AG_FILEDLG_COMPACT));
	if (label)
		AG_TextboxSetLabelS(fd->textbox, label);

	if (AG_ConfigGetPath(AG_CONFIG_PATH_DATA, 0, path, sizeof(path))
	    < sizeof(path)) {
		AG_FileDlgSetDirectoryMRU(fd, mruKey, path);
	}
	return (fd);
}

AG_FileDlg *
AG_FileDlgNew(void *parent, Uint flags)
{
	AG_FileDlg *fd;

	fd = Malloc(sizeof(AG_FileDlg));
	AG_ObjectInit(fd, &agFileDlgClass);

	fd->flags |= flags;
	if (flags & AG_FILEDLG_HFILL) { WIDGET(fd)->flags |= AG_WIDGET_HFILL; }
	if (flags & AG_FILEDLG_VFILL) { WIDGET(fd)->flags |= AG_WIDGET_VFILL; }

	if (flags & AG_FILEDLG_MULTI) {
		if (fd->tlFiles)
			fd->tlFiles->flags |= AG_TLIST_MULTI;
	}
	if (flags & AG_FILEDLG_COMPACT) {
		/*
		 * Compact Mode
		 */
		fd->textbox = AG_TextboxNewS(fd,
		    AG_TEXTBOX_EXCL | AG_TEXTBOX_HFILL,
		    _("File:"));
		AG_TextboxBindUTF8(fd->textbox, fd->cfile, sizeof(fd->cfile));
		AG_TextboxSizeHint(fd->textbox, "<XXXXXXXXXXXXXXXXXXXXXXXXXXXXX>");

		fd->btnExpand = AG_ButtonNewS(fd, AG_BUTTON_NO_FOCUS, "...");
		AG_SetPadding(fd->btnExpand, "1");

		AG_SetEvent(fd->btnExpand, "button-pushed",
		    CompactExpand,"%p",fd);

		AG_ObjectAttach(parent, fd);
		return (fd);
	}

	/* Horizontal divider */
	fd->hPane = AG_PaneNewHoriz(fd, AG_PANE_EXPAND);
	AG_PaneMoveDividerPct(fd->hPane, 50);
	AG_PaneResizeAction(fd->hPane, AG_PANE_DIVIDE_EVEN);

	/* Shortcuts combo. */
	fd->comLoc = AG_ComboNewFn(fd->hPane->div[0], AG_COMBO_HFILL, NULL,
	    LocExpanded,"%p",fd);
	AG_SetEvent(fd->comLoc, "combo-selected", LocSelected,"%p",fd);
	AG_ComboSizeHint(fd->comLoc, "XXXXXXXXXXXXXXXXXXXXXXXXXXXX", 5);

	/* Directories list. */
	fd->tlDirs = AG_TlistNew(fd->hPane->div[0], AG_TLIST_EXPAND);
	AG_SetEvent(fd->tlDirs, "tlist-dblclick", DirSelected, "%p", fd);
	AG_TlistSizeHint(fd->tlDirs, "XXXXXXXXXXXXXX", 8);

	/* Files list. */
	fd->tlFiles = AG_TlistNew(fd->hPane->div[1], AG_TLIST_EXPAND);
	AG_TlistSizeHint(fd->tlFiles, "XXXXXXXXXXXXXXXXXX", 8);
	AG_SetEvent(fd->tlFiles, "tlist-selected", FileSelected, "%p", fd);
	AG_SetEvent(fd->tlFiles, "tlist-dblclick", FileDblClicked, "%p", fd);

	/* Current directory label. */
	fd->lbCwd = AG_LabelNewPolled(fd, AG_LABEL_HFILL,
	    ("Directory: " AGSI_PATH "%s" AGSI_RST),
	    &fd->cwd[0]);
	AG_LabelSizeHint(fd->lbCwd, 1, _("Directory: XXXXXXXXXXXXX"));

	/* Manual file/directory entry textbox. */
	fd->tbFile = AG_TextboxNewS(fd, AG_TEXTBOX_EXCL, _("File: "));
	AG_SetEvent(fd->tbFile, "textbox-postchg", TextboxChanged, "%p", fd);
	AG_SetEvent(fd->tbFile, "textbox-return", TextboxReturn, "%p", fd);

	/* File type selector */
	if (!(flags & AG_FILEDLG_NOTYPESELECT)) {
		fd->comTypes = AG_ComboNewFn(fd, AG_COMBO_HFILL, _("Type: "),
		    FileTypesExpanded,"%p",fd);

		AG_SetEvent(fd->comTypes, "combo-selected",
		    FileTypeSelected,"%p",fd);

		fd->comTypes->nVisItems = 5;
	}
	/* "Mask files" checkboxes. */
	if (!(flags & AG_FILEDLG_NOMASKOPTS)) {
		AG_Checkbox *cb;

		fd->cbMaskExt = cb = AG_CheckboxNewFlag(fd, AG_CHECKBOX_EXCL,
		    _("Mask files by extension"),
		    &fd->flags, AG_FILEDLG_MASK_EXT);
		AG_SetEvent(cb, "checkbox-changed", MaskOptionSelected,"%p",fd);
		AG_SetFontSize(cb, "80%");
	
		fd->cbMaskHidden = cb = AG_CheckboxNewFlag(fd, AG_CHECKBOX_EXCL,
		    _("Mask hidden files"),
		    &fd->flags, AG_FILEDLG_MASK_HIDDEN);
		AG_SetEvent(cb, "checkbox-changed", MaskOptionSelected,"%p",fd);
		AG_SetFontSize(cb, "80%");
	}

	if (!(flags & AG_FILEDLG_NOBUTTONS)) {
		fd->btnOk = AG_ButtonNewS(fd, AG_BUTTON_EXCL, _("OK"));
		AG_SetEvent(fd->btnOk, "button-pushed", PressedOK, "%p", fd);

		fd->btnCancel = AG_ButtonNewS(fd, AG_BUTTON_EXCL, _("Cancel"));
		AG_SetEvent(fd->btnCancel, "button-pushed", PressedCancel, "%p", fd);
	}
	AG_ObjectAttach(parent, fd);
	return (fd);
}

static void
OnDetach(AG_Event *_Nonnull event)
{
	AG_FileDlg *fd = AG_FILEDLG_SELF();

	if (fd->openDir) {
		AG_CloseDir(fd->openDir);
		fd->openDir = NULL;
	}
}

static void
Init(void *_Nonnull obj)
{
	AG_FileDlg *fd = obj;

	fd->flags = AG_FILEDLG_RESET_ONSHOW;
	(void)AG_GetCWD(fd->cwd, sizeof(fd->cwd));
	fd->cfile[0] = '\0';
	fd->fdDir = -1;

	memset(&fd->openDir, 0, sizeof(AG_Dir *) +       /* openDir */
	                        sizeof(AG_EventSink *) + /* esFollow */
	                        sizeof(AG_Pane *) +      /* hPane */
	                        sizeof(AG_Tlist *) +     /* tlDirs */
	                        sizeof(AG_Tlist *) +     /* tlFiles */
	                        sizeof(AG_Label *) +     /* lbCwd */
	                        sizeof(AG_Textbox *) +   /* tbFile */
	                        sizeof(AG_Combo *) +     /* comTypes */
	                        sizeof(AG_Checkbox *) +  /* cbMaskExt */
	                        sizeof(AG_Checkbox *) +  /* cbMaskHidden */
	                        sizeof(AG_Button *) +    /* btnOk */
	                        sizeof(AG_Button *) +    /* btnCancel */
	                        sizeof(AG_Event *) +     /* okAction */
	                        sizeof(AG_Event *) +     /* cancelAction */
	                        sizeof(char *) +         /* dirMRU */
	                        sizeof(void *) +         /* optsCtr */
	                        sizeof(AG_FileType *) +  /* curType */
	                        sizeof(AG_Combo *) +     /* comLoc */
	                        sizeof(AG_Textbox *) +   /* textbox */
	                        sizeof(AG_Button *) +    /* btnExpand */
	                        sizeof(AG_Window *) +    /* winExpand */
	                        sizeof(AG_FileDlg *));   /* fdExpand */

	TAILQ_INIT(&fd->types);

	AG_AddEvent(fd, "widget-shown", OnShow, NULL);
	AG_AddEvent(fd, "widget-hidden", OnHide, NULL);
	AG_AddEvent(fd, "detached", OnDetach, NULL);
}

/*
 * Register an event handler for the "OK" / "Cancel" buttons.
 * This overrides type-specific handlers.
 */
void
AG_FileDlgOkAction(AG_FileDlg *fd, AG_EventFn fn, const char *fmt, ...)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);

	if (fd->okAction) {
		AG_UnsetEvent(fd, fd->okAction->name);
	}
	fd->okAction = AG_SetEvent(fd, NULL, fn, NULL);
	if (fmt) {
		va_list ap;

		va_start(ap, fmt);
		AG_EventGetArgs(fd->okAction, fmt, ap);
		va_end(ap);
	}
#if 0
	if (fd->flags & AG_FILEDLG_ASYNC)
		fd->okAction->flags |= AG_EVENT_ASYNC;
#endif
	AG_ObjectUnlock(fd);
}
void
AG_FileDlgCancelAction(AG_FileDlg *fd, AG_EventFn fn, const char *fmt, ...)
{
	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");
	AG_ObjectLock(fd);

	if (fd->cancelAction) {
		AG_UnsetEvent(fd, fd->cancelAction->name);
	}
	fd->cancelAction = AG_SetEvent(fd, NULL, fn, NULL);
	if (fmt) {
		va_list ap;

		va_start(ap, fmt);
		AG_EventGetArgs(fd->cancelAction, fmt, ap);
		va_end(ap);
	}

	AG_ObjectUnlock(fd);
}

static void
Destroy(void *_Nonnull obj)
{
	AG_FileDlg *fd = obj;
	AG_FileType *ft, *ft2;
	AG_FileOption *fo, *fo2;
	Uint i;

	for (ft = TAILQ_FIRST(&fd->types);
	     ft != TAILQ_END(&fd->types);
	     ft = ft2) {
		ft2 = TAILQ_NEXT(ft, types);
	
		for (fo = TAILQ_FIRST(&ft->opts);
		     fo != TAILQ_END(&ft->opts);
		     fo = fo2) {
			fo2 = TAILQ_NEXT(fo, opts);
			free(fo->descr);
			free(fo->key);
			free(fo);
		}
		free(ft->descr);
		for (i = 0; i < ft->nExts; i++) {
			free(ft->exts[i]);
		}
		free(ft->exts);
		free(ft->allExts);
		free(ft);
	}
	Free(fd->dirMRU);
}

static void
Draw(void *_Nonnull obj)
{
	AG_Widget *chld;

	OBJECT_FOREACH_CHILD(chld, obj, ag_widget)
		AG_WidgetDraw(chld);
}

static void
SizeRequest(void *_Nonnull obj, AG_SizeReq *_Nonnull r)
{
	AG_FileDlg *fd = obj;
	AG_SizeReq rc;
	const int spacingVert = WIDGET(fd)->spacingVert;

	if (fd->flags & AG_FILEDLG_COMPACT) {               /* Compact mode */
		AG_WidgetSizeReq(fd->textbox, &rc);
		r->w = rc.w;
		r->h = rc.h;
		AG_WidgetSizeReq(fd->btnExpand, &rc);
		r->w += rc.w;
		if (rc.h > r->h) { r->h = rc.h; }
		return;
	}

	AG_WidgetSizeReq(fd->hPane, &rc);
	r->w = rc.w;
	r->h = rc.h + spacingVert;
	AG_WidgetSizeReq(fd->lbCwd, &rc);
	r->h += rc.h + spacingVert;
	AG_WidgetSizeReq(fd->tbFile, &rc);
	r->h += rc.h + spacingVert;
	if (fd->comTypes) {
		AG_WidgetSizeReq(fd->comTypes, &rc);
		r->h += rc.h + spacingVert;
	}
	if (!(fd->flags & AG_FILEDLG_NOMASKOPTS)) {
		AG_WidgetSizeReq(fd->cbMaskExt, &rc);
		r->h += rc.h + spacingVert;
		AG_WidgetSizeReq(fd->cbMaskHidden, &rc);
		r->h += rc.h + spacingVert;
	}
	if (!(fd->flags & AG_FILEDLG_NOBUTTONS)) {
		AG_SizeReq rOk, rCancel;

		AG_WidgetSizeReq(fd->btnOk, &rOk);
		AG_WidgetSizeReq(fd->btnCancel, &rCancel);

		r->h += spacingVert*2;
		r->h += MAX(rOk.h, rCancel.h) + 1;
	}
}

static int
SizeAllocate(void *_Nonnull obj, const AG_SizeAlloc *_Nonnull aFD)
{
	AG_FileDlg *fd = obj;
	AG_SizeReq r;
	AG_SizeAlloc a;
	const int paddingLeft  = WIDGET(fd)->paddingLeft;
	const int paddingRight = WIDGET(fd)->paddingRight;
	const int paddingTop   = WIDGET(fd)->paddingTop;
	const int spacingVert  = WIDGET(fd)->spacingVert;
	int hBtn;
	
	if (fd->flags & AG_FILEDLG_COMPACT) {               /* Compact mode */
		AG_WidgetSizeReq(fd->textbox, &r);
		hBtn = r.h;
		AG_WidgetSizeReq(fd->btnExpand, &r);
		hBtn = MAX(hBtn, r.h);
		a.x = 0;
		a.y = 0;
		a.w = aFD->w - r.w;
		a.h = MIN(hBtn, aFD->h);
		AG_WidgetSizeAlloc(fd->textbox, &a);
		a.x += a.w;                                /* aFD->w - r.w */
		a.w = r.w;
		AG_WidgetSizeAlloc(fd->btnExpand, &a);
		return (0);
	}

	if ((fd->flags & AG_FILEDLG_NOBUTTONS) == 0) {     /* OK and Cancel */
		AG_WidgetSizeReq(fd->btnOk, &r);
		hBtn = r.h;
		AG_WidgetSizeReq(fd->btnCancel, &r);
		hBtn = MAX(hBtn, r.h);
	} else {
		hBtn = 0;
	}

	a.x = paddingLeft;
	a.y = paddingTop;
	a.w = aFD->w - paddingLeft - paddingRight;
	a.h = aFD->h - hBtn - paddingTop - WIDGET(fd)->paddingBottom;

	AG_WidgetSizeReq(fd->lbCwd, &r);
	a.h -= r.h;
	AG_WidgetSizeReq(fd->tbFile, &r);
	a.h -= r.h;

	if (fd->comTypes) {
		AG_WidgetSizeReq(fd->comTypes, &r);
		a.h -= r.h + spacingVert;
	}
	if (!(fd->flags & AG_FILEDLG_NOMASKOPTS)) {
		AG_WidgetSizeReq(fd->cbMaskExt, &r);
		a.h -= r.h + spacingVert;
		AG_WidgetSizeReq(fd->cbMaskHidden, &r);
		a.h -= r.h + spacingVert;
	}
	a.h -= spacingVert;

	if (fd->comTypes)
		a.h -= spacingVert;
	if ((fd->flags & AG_FILEDLG_NOMASKOPTS) == 0)
		a.h -= spacingVert;

	AG_WidgetSizeAlloc(fd->hPane, &a);
	a.y += a.h + spacingVert;
	AG_WidgetSizeReq(fd->lbCwd, &r);
	a.h  = r.h;
	AG_WidgetSizeAlloc(fd->lbCwd, &a);
	a.y += a.h + spacingVert;
	AG_WidgetSizeReq(fd->tbFile, &r);
	a.h  = r.h;
	AG_WidgetSizeAlloc(fd->tbFile, &a);
	a.y += a.h + spacingVert;

	if (fd->comTypes) {                           /* File type selector */
		AG_WidgetSizeReq(fd->comTypes, &r);
		a.h  = r.h;
		AG_WidgetSizeAlloc(fd->comTypes, &a);
		a.y += a.h + spacingVert;
	}
	if ((fd->flags & AG_FILEDLG_NOMASKOPTS) == 0) {   /* Masking options */
		AG_WidgetSizeReq(fd->cbMaskExt, &r);
		a.h  = r.h;
		AG_WidgetSizeAlloc(fd->cbMaskExt, &a); 
		a.y += a.h + spacingVert;
		AG_WidgetSizeReq(fd->cbMaskHidden, &r);
		a.h  = r.h;
		AG_WidgetSizeAlloc(fd->cbMaskHidden, &a);
		a.y += a.h + spacingVert;
	}

	if ((fd->flags & AG_FILEDLG_NOBUTTONS) == 0) {      /* OK and Cancel */
		const int spacingHoriz = WIDGET(fd)->spacingHoriz;

		a.y += (spacingVert << 1);
		a.w = ((aFD->w - paddingLeft - paddingRight - spacingHoriz) >> 1);
		a.h = hBtn;
		AG_WidgetSizeAlloc(fd->btnOk, &a);
		a.x = paddingLeft + a.w + spacingHoriz;
		AG_WidgetSizeAlloc(fd->btnCancel, &a);
	}
	return (0);
}

/*
 * Register a common action for all image file formats supported by
 * AG_SurfaceFromFile().
 */
void
AG_FileDlgAddImageTypes(AG_FileDlg *fd, AG_EventFn fn, const char *fmt, ...)
{
	AG_FileType *ftBMP;

	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");

	ftBMP = AG_FileDlgAddType(fd, _("Windows Bitmap"), "*.bmp", NULL, NULL);
	if (fn) {
		ftBMP->action = AG_SetEvent(fd, NULL, fn, NULL);
		if (fmt) {
			va_list ap;

			va_start(ap, fmt);
			AG_EventGetArgs(ftBMP->action, fmt, ap);
			va_end(ap);
		}
	}
#ifdef HAVE_JPEG
	{
		AG_FileType *ft;

		ft = AG_FileDlgAddType(fd, _("JPEG image"), "*.jpg,*.jpeg", NULL, NULL);
		ft->action = AG_EventDup(ftBMP->action);
	}
#endif
#ifdef HAVE_PNG
	{
		AG_FileType *ft;

		ft = AG_FileDlgAddType(fd, _("PNG image"), "*.png", NULL, NULL);
		ft->action = AG_EventDup(ftBMP->action);
	}
#endif
}

/*
 * Inherit the set of FileTypes (and FileOptions) from another FileDlg.
 */
void
AG_FileDlgCopyTypes(AG_FileDlg *fdDst, const AG_FileDlg *fdSrc)
{
	AG_FileType *st, *dt;		/* Source type, destination type */
	const AG_FileOption *so;	/* Source option */

	AG_OBJECT_ISA(fdSrc, "AG_Widget:AG_FileDlg:*");
	AG_OBJECT_ISA(fdDst, "AG_Widget:AG_FileDlg:*");
		
	TAILQ_FOREACH(st, &fdSrc->types, types) {
		dt = AG_FileDlgAddType(fdDst, st->descr, st->allExts, NULL,NULL);
		if (st->action != NULL) {
			dt->action = Malloc(sizeof(AG_Event));
			AG_EventCopy(dt->action, st->action);
		}
	
		TAILQ_FOREACH(so, &st->opts, opts) {
			switch (so->type) {
			case AG_FILEDLG_BOOL:
				AG_FileOptionNewBool(dt, so->descr, so->key,
				    so->data.i.val);
				break;
			case AG_FILEDLG_INT:
				AG_FileOptionNewInt(dt, so->descr, so->key,
				    so->data.i.val,
				    so->data.i.min,
				    so->data.i.max);
				break;
			case AG_FILEDLG_FLOAT:
				AG_FileOptionNewFlt(dt, so->descr, so->key,
				    so->data.flt.val,
				    so->data.flt.min,
				    so->data.flt.max,
				    so->unit);
				break;
			case AG_FILEDLG_DOUBLE:
				AG_FileOptionNewDbl(dt, so->descr, so->key,
				    so->data.dbl.val,
				    so->data.dbl.min,
				    so->data.dbl.max,
				    so->unit);
				break;
			case AG_FILEDLG_STRING:
				AG_FileOptionNewString(dt, so->descr, so->key,
				    so->data.s);
				break;
			}
		}
	}
}

/* Register a new file type and callback function. */
AG_FileType *
AG_FileDlgAddType(AG_FileDlg *fd, const char *descr, const char *exts,
    AG_EventFn fn, const char *fmt, ...)
{
	AG_FileType *ft;
	char *dexts, *ds, *ext;

	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");

	ft = Malloc(sizeof(AG_FileType));
	ft->fd = fd;
	ft->descr = Strdup(descr);
	ft->exts = Malloc(sizeof(char *));
	ft->nExts = 0;
	ft->flags = 0;
	TAILQ_INIT(&ft->opts);

	/* Duplicate the full list of extensions into allExts. */
	ft->allExts = Strdup(exts);

	/* Also generate an array of extensions exts[]. */
	ds = dexts = Strdup(exts);
	while ((ext = AG_Strsep(&ds, ",;")) != NULL) {
		if (ext[0] == '*' && ext[1] == '.') {
			ext++;
		}
		ft->exts = Realloc(ft->exts, (ft->nExts + 1) * sizeof(char *));
		ft->exts[ft->nExts++] = Strdup(ext);
	}
	free(dexts);
	
	AG_ObjectLock(fd);

	/* Set the type-specific callback function. */
	if (fn) {
		ft->action = AG_SetEvent(fd, NULL, fn, NULL);
		if (fmt) {
			va_list ap;

			va_start(ap, fmt);
			AG_EventGetArgs(ft->action, fmt, ap);
			va_end(ap);
		}
#if 0
		if (fd->flags & AG_FILEDLG_ASYNC)
			ft->action->flags |= AG_EVENT_ASYNC;
#endif
	} else {
		ft->action = NULL;
	}

	TAILQ_INSERT_TAIL(&fd->types, ft, types);
	
	AG_ObjectUnlock(fd);
	return (ft);
}

AG_FileOption *
AG_FileOptionNewBool(AG_FileType *ft, const char *descr, const char *key,
    int dflt)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fto;

	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");

	fto = Malloc(sizeof(AG_FileOption));
	fto->descr = Strdup(descr);
	fto->key = Strdup(key);
	fto->unit = NULL;
	fto->type = AG_FILEDLG_BOOL;
	fto->data.i.val = dflt;
	
	AG_ObjectLock(fd);
	TAILQ_INSERT_TAIL(&ft->opts, fto, opts);
	AG_ObjectUnlock(fd);

	return (fto);
}

AG_FileOption *
AG_FileOptionNewInt(AG_FileType *ft, const char *descr, const char *key,
    int dflt, int min, int max)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fto;

	AG_OBJECT_ISA(fd, "AG_Widget:AG_FileDlg:*");

	fto = Malloc(sizeof(AG_FileOption));
	fto->descr = Strdup(descr);
	fto->key = Strdup(key);
	fto->unit = NULL;
	fto->type = AG_FILEDLG_INT;
	fto->data.i.val = dflt;
	fto->data.i.min = min;
	fto->data.i.max = max;
	
	AG_ObjectLock(fd);
	TAILQ_INSERT_TAIL(&ft->opts, fto, opts);
	AG_ObjectUnlock(fd);

	return (fto);
}

AG_FileOption *
AG_FileOptionNewFlt(AG_FileType *ft, const char *descr, const char *key,
    float dflt, float min, float max, const char *unit)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fto;

	fto = Malloc(sizeof(AG_FileOption));
	fto->descr = Strdup(descr);
	fto->key = Strdup(key);
	fto->unit = (unit != NULL) ? Strdup(unit) : NULL;
	fto->type = AG_FILEDLG_FLOAT;
	fto->data.flt.val = dflt;
	fto->data.flt.min = min;
	fto->data.flt.max = max;
	
	AG_ObjectLock(fd);
	TAILQ_INSERT_TAIL(&ft->opts, fto, opts);
	AG_ObjectUnlock(fd);

	return (fto);
}

AG_FileOption *
AG_FileOptionNewDbl(AG_FileType *ft, const char *descr, const char *key,
    double dflt, double min, double max, const char *unit)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fto;

	fto = Malloc(sizeof(AG_FileOption));
	fto->descr = Strdup(descr);
	fto->key = Strdup(key);
	fto->unit = (unit != NULL) ? Strdup(unit) : NULL;
	fto->type = AG_FILEDLG_DOUBLE;
	fto->data.dbl.val = dflt;
	fto->data.dbl.min = min;
	fto->data.dbl.max = max;
	
	AG_ObjectLock(fd);
	TAILQ_INSERT_TAIL(&ft->opts, fto, opts);
	AG_ObjectUnlock(fd);

	return (fto);
}

AG_FileOption *
AG_FileOptionNewString(AG_FileType *ft, const char *descr, const char *key,
    const char *dflt)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fto;

	fto = Malloc(sizeof(AG_FileOption));
	fto->descr = Strdup(descr);
	fto->key = Strdup(key);
	fto->unit = NULL;
	fto->type = AG_FILEDLG_STRING;
	Strlcpy(fto->data.s, dflt, sizeof(fto->data.s));
	
	AG_ObjectLock(fd);
	TAILQ_INSERT_TAIL(&ft->opts, fto, opts);
	AG_ObjectUnlock(fd);

	return (fto);
}

/* The FileDlg must be locked. */
AG_FileOption *
AG_FileOptionGet(AG_FileType *ft, const char *key)
{
	AG_FileOption *fo;

	TAILQ_FOREACH(fo, &ft->opts, opts) {
		if (strcmp(fo->key, key) == 0)
			break;
	}
	return (fo);
}

int
AG_FileOptionInt(AG_FileType *ft, const char *key)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fo;
	int rv;

	AG_ObjectLock(fd);

	fo = AG_FileOptionGet(ft, key);
	rv = (fo != NULL) ? fo->data.i.val : -1;

	AG_ObjectUnlock(fd);
	return (rv);
}

int
AG_FileOptionBool(AG_FileType *ft, const char *key)
{
	return AG_FileOptionInt(ft, key);
}

float
AG_FileOptionFlt(AG_FileType *ft, const char *key)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fo;
	float rv;

	AG_ObjectLock(fd);

	fo = AG_FileOptionGet(ft, key);
	rv = (fo != NULL) ? fo->data.flt.val : 0.0;

	AG_ObjectUnlock(fd);
	return (rv);
}

double
AG_FileOptionDbl(AG_FileType *ft, const char *key)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fo;
	double rv;

	AG_ObjectLock(fd);

	fo = AG_FileOptionGet(ft, key);
	rv = (fo != NULL) ? fo->data.dbl.val : 0.0;

	AG_ObjectUnlock(fd);
	return (rv);
}

char *
AG_FileOptionString(AG_FileType *ft, const char *key)
{
	AG_FileDlg *fd = ft->fd;
	AG_FileOption *fo;
	char *rv;

	AG_ObjectLock(fd);

	fo = AG_FileOptionGet(ft, key);
	rv = (fo != NULL) ? fo->data.s : "";

	AG_ObjectUnlock(fd);
	return (rv);
}

AG_WidgetClass agFileDlgClass = {
	{
		"Agar(Widget:FileDlg)",
		sizeof(AG_FileDlg),
		{ 1,0, AGC_FILE_DLG, 0x1F4BE },
		Init,
		NULL,		/* reset */
		Destroy,
		NULL,		/* load */
		NULL,		/* save */
		NULL		/* edit */
	},
	Draw,
	SizeRequest,
	SizeAllocate,
	NULL,			/* mouse_button_down */
	NULL,			/* mouse_button_up */
	NULL,			/* mouse_motion */
	NULL,			/* key_down */
	NULL,			/* key_up */
	NULL,			/* touch */
	NULL,			/* ctrl */
	NULL			/* joy */
};

#endif /* AG_WIDGETS */
