1/*
2 * tkWinDialog.c --
3 *
4 *	Contains the Windows implementation of the common dialog boxes.
5 *
6 * Copyright (c) 1996-1997 Sun Microsystems, Inc.
7 *
8 * See the file "license.terms" for information on usage and redistribution of
9 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
10 *
11 * RCS: @(#) $Id$
12 *
13 */
14
15#define WINVER        0x0500   /* Requires Windows 2K definitions */
16#define _WIN32_WINNT  0x0500
17#include "tkWinInt.h"
18#include "tkFileFilter.h"
19
20#include <commdlg.h>		/* includes common dialog functionality */
21#ifdef _MSC_VER
22#   pragma comment (lib, "comdlg32.lib")
23#endif
24#include <dlgs.h>		/* includes common dialog template defines */
25#include <cderr.h>		/* includes the common dialog error codes */
26
27#include <shlobj.h>		/* includes SHBrowseForFolder */
28#ifdef _MSC_VER
29#   pragma comment (lib, "shell32.lib")
30#endif
31
32/* These needed for compilation with VC++ 5.2 */
33#ifndef BIF_EDITBOX
34#define BIF_EDITBOX 0x10
35#endif
36
37#ifndef BIF_VALIDATE
38#define BIF_VALIDATE 0x0020
39#endif
40
41#ifndef BIF_NEWDIALOGSTYLE
42#define BIF_NEWDIALOGSTYLE 0x0040
43#endif
44
45#ifndef BFFM_VALIDATEFAILED
46#ifdef UNICODE
47#define BFFM_VALIDATEFAILED 4
48#else
49#define BFFM_VALIDATEFAILED 3
50#endif
51#endif /* BFFM_VALIDATEFAILED */
52
53#ifndef OPENFILENAME_SIZE_VERSION_400
54#define OPENFILENAME_SIZE_VERSION_400 76
55#endif
56
57/*
58 * The following structure is used by the new Tk_ChooseDirectoryObjCmd to pass
59 * data between it and its callback. Unqiue to Winodws platform.
60 */
61
62typedef struct ChooseDirData {
63   TCHAR utfInitDir[MAX_PATH];	/* Initial folder to use */
64   TCHAR utfRetDir[MAX_PATH];	/* Returned folder to use */
65   Tcl_Interp *interp;
66   int mustExist;		/* True if file must exist to return from
67				 * callback */
68} CHOOSEDIRDATA;
69
70typedef struct ThreadSpecificData {
71    int debugFlag;		/* Flags whether we should output debugging
72				 * information while displaying a builtin
73				 * dialog. */
74    Tcl_Interp *debugInterp;	/* Interpreter to used for debugging. */
75    UINT WM_LBSELCHANGED;	/* Holds a registered windows event used for
76				 * communicating between the Directory Chooser
77				 * dialog and its hook proc. */
78    HHOOK hMsgBoxHook;		/* Hook proc for tk_messageBox and the */
79    HICON hSmallIcon;		/* icons used by a parent to be used in */
80    HICON hBigIcon;		/* the message box */
81} ThreadSpecificData;
82static Tcl_ThreadDataKey dataKey;
83
84/*
85 * The following structures are used by Tk_MessageBoxCmd() to parse arguments
86 * and return results.
87 */
88
89static const TkStateMap iconMap[] = {
90    {MB_ICONERROR,		"error"},
91    {MB_ICONINFORMATION,	"info"},
92    {MB_ICONQUESTION,		"question"},
93    {MB_ICONWARNING,		"warning"},
94    {-1,			NULL}
95};
96
97static const TkStateMap typeMap[] = {
98    {MB_ABORTRETRYIGNORE,	"abortretryignore"},
99    {MB_OK,			"ok"},
100    {MB_OKCANCEL,		"okcancel"},
101    {MB_RETRYCANCEL,		"retrycancel"},
102    {MB_YESNO,			"yesno"},
103    {MB_YESNOCANCEL,		"yesnocancel"},
104    {-1,			NULL}
105};
106
107static const TkStateMap buttonMap[] = {
108    {IDABORT,			"abort"},
109    {IDRETRY,			"retry"},
110    {IDIGNORE,			"ignore"},
111    {IDOK,			"ok"},
112    {IDCANCEL,			"cancel"},
113    {IDNO,			"no"},
114    {IDYES,			"yes"},
115    {-1,			NULL}
116};
117
118static const int buttonFlagMap[] = {
119    MB_DEFBUTTON1, MB_DEFBUTTON2, MB_DEFBUTTON3, MB_DEFBUTTON4
120};
121
122static const struct {int type; int btnIds[3];} allowedTypes[] = {
123    {MB_ABORTRETRYIGNORE,	{IDABORT, IDRETRY,  IDIGNORE}},
124    {MB_OK,			{IDOK,	  -1,	    -1	    }},
125    {MB_OKCANCEL,		{IDOK,	  IDCANCEL, -1	    }},
126    {MB_RETRYCANCEL,		{IDRETRY, IDCANCEL, -1	    }},
127    {MB_YESNO,			{IDYES,	  IDNO,	    -1	    }},
128    {MB_YESNOCANCEL,		{IDYES,	  IDNO,	    IDCANCEL}}
129};
130
131#define NUM_TYPES (sizeof(allowedTypes) / sizeof(allowedTypes[0]))
132
133/*
134 * Abstract trivial differences between Win32 and Win64.
135 */
136
137#define TkWinGetHInstance(from) \
138	((HINSTANCE) GetWindowLongPtr((from), GWLP_HINSTANCE))
139#define TkWinGetUserData(from) \
140	GetWindowLongPtr((from), GWLP_USERDATA)
141#define TkWinSetUserData(to,what) \
142	SetWindowLongPtr((to), GWLP_USERDATA, (LPARAM)(what))
143
144/*
145 * The value of TK_MULTI_MAX_PATH dictactes how many files can be retrieved
146 * with tk_get*File -multiple 1. It must be allocated on the stack, so make it
147 * large enough but not too large. - hobbs
148 *
149 * The data is stored as <dir>\0<file1>\0<file2>\0...<fileN>\0\0. Since
150 * MAX_PATH == 260 on Win2K/NT, *40 is ~10Kbytes.
151 */
152
153#define TK_MULTI_MAX_PATH	(MAX_PATH*40)
154
155/*
156 * The following structure is used to pass information between the directory
157 * chooser function, Tk_ChooseDirectoryObjCmd(), and its dialog hook proc.
158 */
159
160typedef struct ChooseDir {
161    Tcl_Interp *interp;		/* Interp, used only if debug is turned on,
162				 * for setting the "tk_dialog" variable. */
163    int lastCtrl;		/* Used by hook proc to keep track of last
164				 * control that had input focus, so when OK is
165				 * pressed we know whether to browse a new
166				 * directory or return. */
167    int lastIdx;		/* Last item that was selected in directory
168				 * browser listbox. */
169    TCHAR path[MAX_PATH];	/* On return from choose directory dialog,
170				 * holds the selected path. Cannot return
171				 * selected path in ofnPtr->lpstrFile because
172				 * the default dialog proc stores a '\0' in
173				 * it, since, of course, no _file_ was
174				 * selected. */
175    OPENFILENAME *ofnPtr;	/* pointer to the OFN structure */
176} ChooseDir;
177
178/*
179 * The following structure is used to pass information between GetFileName/W
180 * functions and OFN dialog hook procedures. [Bug 2896501, Patch 2898255]
181 */
182
183typedef struct OFNData {
184    Tcl_Interp *interp;		/* Interp, used only if debug is turned on,
185				 * for setting the "tk_dialog" variable. */
186    int dynFileBufferSize;	/* Dynamic filename buffer size, stored to
187				 * avoid shrinking and expanding the buffer
188				 * when selection changes */
189    char *dynFileBuffer;	/* Dynamic filename buffer, cast to WCHAR* in
190				 * UNICODE procedures */
191} OFNData;
192
193/*
194 * Definitions of functions used only in this file.
195 */
196
197static UINT APIENTRY	ChooseDirectoryValidateProc(HWND hdlg, UINT uMsg,
198			    LPARAM wParam, LPARAM lParam);
199static UINT CALLBACK	ColorDlgHookProc(HWND hDlg, UINT uMsg, WPARAM wParam,
200			    LPARAM lParam);
201static int 		GetFileNameA(ClientData clientData,
202			    Tcl_Interp *interp, int objc,
203			    Tcl_Obj *CONST objv[], int isOpen);
204static int 		GetFileNameW(ClientData clientData,
205			    Tcl_Interp *interp, int objc,
206			    Tcl_Obj *CONST objv[], int isOpen);
207static int 		MakeFilter(Tcl_Interp *interp, Tcl_Obj *valuePtr,
208			    Tcl_DString *dsPtr, Tcl_Obj *initialPtr,
209			    int *index);
210static UINT APIENTRY	OFNHookProcA(HWND hdlg, UINT uMsg, WPARAM wParam,
211			    LPARAM lParam);
212static UINT APIENTRY	OFNHookProcW(HWND hdlg, UINT uMsg, WPARAM wParam,
213			    LPARAM lParam);
214static LRESULT CALLBACK MsgBoxCBTProc(int nCode, WPARAM wParam, LPARAM lParam);
215static void		SetTkDialog(ClientData clientData);
216static char *		ConvertExternalFilename(Tcl_Encoding encoding,
217			    char *filename, Tcl_DString *dsPtr);
218
219/*
220 *-------------------------------------------------------------------------
221 *
222 * EatSpuriousMessageBugFix --
223 *
224 *	In the file open/save dialog, double clicking on a list item causes
225 *	the dialog box to close, but an unwanted WM_LBUTTONUP message is sent
226 *	to the window underneath. If the window underneath happens to be a
227 *	windows control (eg a button) then it will be activated by accident.
228 *
229 * 	This problem does not occur in dialog boxes, because windows must do
230 * 	some special processing to solve the problem. (separate message
231 * 	processing functions are used to cope with keyboard navigation of
232 * 	controls.)
233 *
234 * 	Here is one solution. After returning, we poll the message queue for
235 * 	1/4s looking for WM_LBUTTON up messages. If we see one it's consumed.
236 * 	If we get a WM_LBUTTONDOWN message, then we exit early, since the user
237 * 	must be doing something new. This fix only works for the current
238 * 	application, so the problem will still occur if the open dialog
239 * 	happens to be over another applications button. However this is a
240 * 	fairly rare occurrance.
241 *
242 * Results:
243 *	None.
244 *
245 * Side effects:
246 *	Consumes an unwanted BUTTON messages.
247 *
248 *-------------------------------------------------------------------------
249 */
250
251static void
252EatSpuriousMessageBugFix(void)
253{
254    MSG msg;
255    DWORD nTime = GetTickCount() + 250;
256
257    while (GetTickCount() < nTime) {
258	if (PeekMessage(&msg, 0, WM_LBUTTONDOWN, WM_LBUTTONDOWN, PM_NOREMOVE)){
259	    break;
260	}
261	PeekMessage(&msg, 0, WM_LBUTTONUP, WM_LBUTTONUP, PM_REMOVE);
262    }
263}
264
265/*
266 *-------------------------------------------------------------------------
267 *
268 * TkWinDialogDebug --
269 *
270 *	Function to turn on/off debugging support for common dialogs under
271 *	windows. The variable "tk_debug" is set to the identifier of the
272 *	dialog window when the modal dialog window pops up and it is safe to
273 *	send messages to the dialog.
274 *
275 * Results:
276 *	None.
277 *
278 * Side effects:
279 *	This variable only makes sense if just one dialog is up at a time.
280 *
281 *-------------------------------------------------------------------------
282 */
283
284void
285TkWinDialogDebug(
286    int debug)
287{
288    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
289	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
290
291    tsdPtr->debugFlag = debug;
292}
293
294/*
295 *-------------------------------------------------------------------------
296 *
297 * Tk_ChooseColorObjCmd --
298 *
299 *	This function implements the color dialog box for the Windows
300 *	platform. See the user documentation for details on what it does.
301 *
302 * Results:
303 *	See user documentation.
304 *
305 * Side effects:
306 *	A dialog window is created the first time this function is called.
307 *	This window is not destroyed and will be reused the next time the
308 *	application invokes the "tk_chooseColor" command.
309 *
310 *-------------------------------------------------------------------------
311 */
312
313int
314Tk_ChooseColorObjCmd(
315    ClientData clientData,	/* Main window associated with interpreter. */
316    Tcl_Interp *interp,		/* Current interpreter. */
317    int objc,			/* Number of arguments. */
318    Tcl_Obj *CONST objv[])	/* Argument objects. */
319{
320    Tk_Window tkwin = (Tk_Window) clientData, parent;
321    HWND hWnd;
322    int i, oldMode, winCode, result;
323    CHOOSECOLOR chooseColor;
324    static int inited = 0;
325    static COLORREF dwCustColors[16];
326    static long oldColor;		/* the color selected last time */
327    static CONST char *optionStrings[] = {
328	"-initialcolor", "-parent", "-title", NULL
329    };
330    enum options {
331	COLOR_INITIAL, COLOR_PARENT, COLOR_TITLE
332    };
333
334    result = TCL_OK;
335    if (inited == 0) {
336	/*
337	 * dwCustColors stores the custom color which the user can modify. We
338	 * store these colors in a static array so that the next time the
339	 * color dialog pops up, the same set of custom colors remain in the
340	 * dialog.
341	 */
342
343	for (i = 0; i < 16; i++) {
344	    dwCustColors[i] = RGB(255-i * 10, i, i * 10);
345	}
346	oldColor = RGB(0xa0, 0xa0, 0xa0);
347	inited = 1;
348    }
349
350    parent			= tkwin;
351    chooseColor.lStructSize	= sizeof(CHOOSECOLOR);
352    chooseColor.hwndOwner	= NULL;
353    chooseColor.hInstance	= NULL;
354    chooseColor.rgbResult	= oldColor;
355    chooseColor.lpCustColors	= dwCustColors;
356    chooseColor.Flags		= CC_RGBINIT | CC_FULLOPEN | CC_ENABLEHOOK;
357    chooseColor.lCustData	= (LPARAM) NULL;
358    chooseColor.lpfnHook	= (LPOFNHOOKPROC) ColorDlgHookProc;
359    chooseColor.lpTemplateName	= (LPTSTR) interp;
360
361    for (i = 1; i < objc; i += 2) {
362	int index;
363	char *string;
364	Tcl_Obj *optionPtr, *valuePtr;
365
366	optionPtr = objv[i];
367	valuePtr = objv[i + 1];
368
369	if (Tcl_GetIndexFromObj(interp, optionPtr, optionStrings, "option",
370		TCL_EXACT, &index) != TCL_OK) {
371	    return TCL_ERROR;
372	}
373	if (i + 1 == objc) {
374	    string = Tcl_GetString(optionPtr);
375	    Tcl_AppendResult(interp, "value for \"", string, "\" missing",
376		    NULL);
377	    return TCL_ERROR;
378	}
379
380	string = Tcl_GetString(valuePtr);
381	switch ((enum options) index) {
382	case COLOR_INITIAL: {
383	    XColor *colorPtr;
384
385	    colorPtr = Tk_GetColor(interp, tkwin, string);
386	    if (colorPtr == NULL) {
387		return TCL_ERROR;
388	    }
389	    chooseColor.rgbResult = RGB(colorPtr->red / 0x100,
390		    colorPtr->green / 0x100, colorPtr->blue / 0x100);
391	    break;
392	}
393	case COLOR_PARENT:
394	    parent = Tk_NameToWindow(interp, string, tkwin);
395	    if (parent == NULL) {
396		return TCL_ERROR;
397	    }
398	    break;
399	case COLOR_TITLE:
400	    chooseColor.lCustData = (LPARAM) string;
401	    break;
402	}
403    }
404
405    Tk_MakeWindowExist(parent);
406    chooseColor.hwndOwner = NULL;
407    hWnd = Tk_GetHWND(Tk_WindowId(parent));
408    chooseColor.hwndOwner = hWnd;
409
410    oldMode = Tcl_SetServiceMode(TCL_SERVICE_ALL);
411    winCode = ChooseColor(&chooseColor);
412    (void) Tcl_SetServiceMode(oldMode);
413
414    /*
415     * Ensure that hWnd is enabled, because it can happen that we have updated
416     * the wrapper of the parent, which causes us to leave this child disabled
417     * (Windows loses sync).
418     */
419
420    EnableWindow(hWnd, 1);
421
422    /*
423     * Clear the interp result since anything may have happened during the
424     * modal loop.
425     */
426
427    Tcl_ResetResult(interp);
428
429    /*
430     * 3. Process the result of the dialog
431     */
432
433    if (winCode) {
434	/*
435	 * User has selected a color
436	 */
437	char color[100];
438
439	sprintf(color, "#%02x%02x%02x",
440		GetRValue(chooseColor.rgbResult),
441		GetGValue(chooseColor.rgbResult),
442		GetBValue(chooseColor.rgbResult));
443	Tcl_AppendResult(interp, color, NULL);
444	oldColor = chooseColor.rgbResult;
445	result = TCL_OK;
446    }
447
448    return result;
449}
450
451/*
452 *-------------------------------------------------------------------------
453 *
454 * ColorDlgHookProc --
455 *
456 *	Provides special handling of messages for the Color common dialog box.
457 *	Used to set the title when the dialog first appears.
458 *
459 * Results:
460 *	The return value is 0 if the default dialog box function should handle
461 *	the message, non-zero otherwise.
462 *
463 * Side effects:
464 *	Changes the title of the dialog window.
465 *
466 *----------------------------------------------------------------------
467 */
468
469static UINT CALLBACK
470ColorDlgHookProc(
471    HWND hDlg,			/* Handle to the color dialog. */
472    UINT uMsg,			/* Type of message. */
473    WPARAM wParam,		/* First message parameter. */
474    LPARAM lParam)		/* Second message parameter. */
475{
476    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
477	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
478    const char *title;
479    CHOOSECOLOR *ccPtr;
480
481    if (WM_INITDIALOG == uMsg) {
482
483	/*
484	 * Set the title string of the dialog.
485	 */
486
487	ccPtr = (CHOOSECOLOR *) lParam;
488	title = (const char *) ccPtr->lCustData;
489
490	if ((title != NULL) && (title[0] != '\0')) {
491	    Tcl_DString ds;
492
493	    (*tkWinProcs->setWindowText)(hDlg,
494		    Tcl_WinUtfToTChar(title, -1, &ds));
495	    Tcl_DStringFree(&ds);
496	}
497	if (tsdPtr->debugFlag) {
498	    tsdPtr->debugInterp = (Tcl_Interp *) ccPtr->lpTemplateName;
499	    Tcl_DoWhenIdle(SetTkDialog, (ClientData) hDlg);
500	}
501	return TRUE;
502    }
503    return FALSE;
504}
505
506/*
507 *----------------------------------------------------------------------
508 *
509 * Tk_GetOpenFileCmd --
510 *
511 *	This function implements the "open file" dialog box for the Windows
512 *	platform. See the user documentation for details on what it does.
513 *
514 * Results:
515 *	See user documentation.
516 *
517 * Side effects:
518 *	A dialog window is created the first this function is called.
519 *
520 *----------------------------------------------------------------------
521 */
522
523int
524Tk_GetOpenFileObjCmd(
525    ClientData clientData,	/* Main window associated with interpreter. */
526    Tcl_Interp *interp,		/* Current interpreter. */
527    int objc,			/* Number of arguments. */
528    Tcl_Obj *CONST objv[])	/* Argument objects. */
529{
530    if (TkWinGetPlatformId() == VER_PLATFORM_WIN32_NT) {
531	return GetFileNameW(clientData, interp, objc, objv, 1);
532    } else {
533	return GetFileNameA(clientData, interp, objc, objv, 1);
534    }
535}
536
537/*
538 *----------------------------------------------------------------------
539 *
540 * Tk_GetSaveFileCmd --
541 *
542 *	Same as Tk_GetOpenFileCmd but opens a "save file" dialog box
543 *	instead
544 *
545 * Results:
546 *	Same as Tk_GetOpenFileCmd.
547 *
548 * Side effects:
549 *	Same as Tk_GetOpenFileCmd.
550 *
551 *----------------------------------------------------------------------
552 */
553
554int
555Tk_GetSaveFileObjCmd(
556    ClientData clientData,	/* Main window associated with interpreter. */
557    Tcl_Interp *interp,		/* Current interpreter. */
558    int objc,			/* Number of arguments. */
559    Tcl_Obj *CONST objv[])	/* Argument objects. */
560{
561    if (TkWinGetPlatformId() == VER_PLATFORM_WIN32_NT) {
562	return GetFileNameW(clientData, interp, objc, objv, 0);
563    } else {
564	return GetFileNameA(clientData, interp, objc, objv, 0);
565    }
566}
567
568/*
569 *----------------------------------------------------------------------
570 *
571 * GetFileNameW --
572 *
573 *	Calls GetOpenFileName() or GetSaveFileName().
574 *
575 * Results:
576 *	See user documentation.
577 *
578 * Side effects:
579 *	See user documentation.
580 *
581 *----------------------------------------------------------------------
582 */
583
584static int
585GetFileNameW(
586    ClientData clientData,	/* Main window associated with interpreter. */
587    Tcl_Interp *interp,		/* Current interpreter. */
588    int objc,			/* Number of arguments. */
589    Tcl_Obj *CONST objv[],	/* Argument objects. */
590    int open)			/* 1 to call GetOpenFileName(), 0 to call
591				 * GetSaveFileName(). */
592{
593    OPENFILENAMEW ofn;
594    WCHAR file[TK_MULTI_MAX_PATH];
595    OFNData ofnData;
596    int cdlgerr;
597    int filterIndex = 0, result = TCL_ERROR, winCode, oldMode, i, multi = 0;
598    char *extension = NULL, *filter = NULL, *title = NULL;
599    Tk_Window tkwin = (Tk_Window) clientData;
600    HWND hWnd;
601    Tcl_Obj *filterObj = NULL, *initialTypeObj = NULL, *typeVariableObj = NULL;
602    Tcl_DString utfFilterString, utfDirString, ds;
603    Tcl_DString extString, filterString, dirString, titleString;
604    Tcl_Encoding unicodeEncoding = TkWinGetUnicodeEncoding();
605    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
606	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
607    static CONST char *saveOptionStrings[] = {
608	"-defaultextension", "-filetypes", "-initialdir", "-initialfile",
609	"-parent", "-title", "-typevariable", NULL
610    };
611    static CONST char *openOptionStrings[] = {
612	"-defaultextension", "-filetypes", "-initialdir", "-initialfile",
613	"-multiple", "-parent", "-title", "-typevariable", NULL
614    };
615    CONST char **optionStrings;
616
617    enum options {
618	FILE_DEFAULT,	FILE_TYPES,	FILE_INITDIR,	FILE_INITFILE,
619	FILE_MULTIPLE,	FILE_PARENT,	FILE_TITLE,     FILE_TYPEVARIABLE
620    };
621
622    file[0] = '\0';
623    ZeroMemory(&ofnData, sizeof(OFNData));
624    Tcl_DStringInit(&utfFilterString);
625    Tcl_DStringInit(&utfDirString);
626
627    /*
628     * Parse the arguments.
629     */
630
631    if (open) {
632	optionStrings = openOptionStrings;
633    } else {
634	optionStrings = saveOptionStrings;
635    }
636
637    for (i = 1; i < objc; i += 2) {
638	int index;
639	char *string;
640	Tcl_Obj *optionPtr, *valuePtr;
641
642	optionPtr = objv[i];
643	valuePtr = objv[i + 1];
644
645	if (Tcl_GetIndexFromObj(interp, optionPtr, optionStrings,
646		"option", 0, &index) != TCL_OK) {
647	    goto end;
648	}
649
650	/*
651	 * We want to maximize code sharing between the open and save file
652	 * dialog implementations; in particular, the switch statement below.
653	 * We use different sets of option strings from the GetIndexFromObj
654	 * call above, but a single enumeration for both. The save file dialog
655	 * doesn't support -multiple, but it falls in the middle of the
656	 * enumeration. Ultimately, this means that when the index found by
657	 * GetIndexFromObj is >= FILE_MULTIPLE, when doing a save file dialog,
658	 * we have to increment the index, so that it matches the open file
659	 * dialog enumeration.
660	 */
661
662	if (!open && index >= FILE_MULTIPLE) {
663	    index++;
664	}
665	if (i + 1 == objc) {
666	    string = Tcl_GetString(optionPtr);
667	    Tcl_AppendResult(interp, "value for \"", string, "\" missing",
668		    NULL);
669	    goto end;
670	}
671
672	string = Tcl_GetString(valuePtr);
673	switch ((enum options) index) {
674	case FILE_DEFAULT:
675	    if (string[0] == '.') {
676		string++;
677	    }
678	    extension = string;
679	    break;
680	case FILE_TYPES:
681	    filterObj = valuePtr;
682	    break;
683	case FILE_INITDIR:
684	    Tcl_DStringFree(&utfDirString);
685	    if (Tcl_TranslateFileName(interp, string,
686		    &utfDirString) == NULL) {
687		goto end;
688	    }
689	    break;
690	case FILE_INITFILE:
691	    if (Tcl_TranslateFileName(interp, string, &ds) == NULL) {
692		goto end;
693	    }
694	    Tcl_UtfToExternal(NULL, unicodeEncoding, Tcl_DStringValue(&ds),
695		    Tcl_DStringLength(&ds), 0, NULL, (char *) file,
696		    sizeof(file), NULL, NULL, NULL);
697	    Tcl_DStringFree(&ds);
698	    break;
699	case FILE_MULTIPLE:
700	    if (Tcl_GetBooleanFromObj(interp, valuePtr, &multi) != TCL_OK) {
701		return TCL_ERROR;
702	    }
703	    break;
704	case FILE_PARENT:
705	    tkwin = Tk_NameToWindow(interp, string, tkwin);
706	    if (tkwin == NULL) {
707		goto end;
708	    }
709	    break;
710	case FILE_TITLE:
711	    title = string;
712	    break;
713	case FILE_TYPEVARIABLE:
714	    typeVariableObj = valuePtr;
715	    initialTypeObj = Tcl_ObjGetVar2(interp, typeVariableObj, NULL,
716		    TCL_GLOBAL_ONLY);
717	    break;
718	}
719    }
720
721    if (MakeFilter(interp, filterObj, &utfFilterString, initialTypeObj,
722	    &filterIndex) != TCL_OK) {
723	goto end;
724    }
725    filter = Tcl_DStringValue(&utfFilterString);
726
727    Tk_MakeWindowExist(tkwin);
728    hWnd = Tk_GetHWND(Tk_WindowId(tkwin));
729
730    ZeroMemory(&ofn, sizeof(OPENFILENAMEW));
731    if (LOBYTE(LOWORD(GetVersion())) < 5) {
732	ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400;
733    } else {
734	ofn.lStructSize = sizeof(OPENFILENAMEW);
735    }
736    ofn.hwndOwner = hWnd;
737    ofn.hInstance = TkWinGetHInstance(ofn.hwndOwner);
738    ofn.lpstrFile = (WCHAR *) file;
739    ofn.nMaxFile = TK_MULTI_MAX_PATH;
740    ofn.Flags = OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR
741	    | OFN_EXPLORER | OFN_ENABLEHOOK;
742    ofn.lpfnHook = (LPOFNHOOKPROC) OFNHookProcW;
743    ofn.lCustData = (LPARAM) &ofnData;
744
745    if (open != 0) {
746	ofn.Flags |= OFN_FILEMUSTEXIST;
747    } else {
748	ofn.Flags |= OFN_OVERWRITEPROMPT;
749    }
750    if (tsdPtr->debugFlag != 0) {
751	ofnData.interp = interp;
752    }
753    if (multi != 0) {
754	ofn.Flags |= OFN_ALLOWMULTISELECT;
755
756	/*
757	 * Starting buffer size. The buffer will be expanded by the OFN dialog
758	 * procedure when necessary
759	 */
760
761	ofnData.dynFileBufferSize = 1024;
762	ofnData.dynFileBuffer = ckalloc(1024);
763    }
764
765    if (extension != NULL) {
766	Tcl_UtfToExternalDString(unicodeEncoding, extension, -1, &extString);
767	ofn.lpstrDefExt = (WCHAR *) Tcl_DStringValue(&extString);
768    }
769
770    Tcl_UtfToExternalDString(unicodeEncoding,
771	    Tcl_DStringValue(&utfFilterString),
772	    Tcl_DStringLength(&utfFilterString), &filterString);
773    ofn.lpstrFilter = (WCHAR *) Tcl_DStringValue(&filterString);
774    ofn.nFilterIndex = filterIndex;
775
776    if (Tcl_DStringValue(&utfDirString)[0] != '\0') {
777	Tcl_UtfToExternalDString(unicodeEncoding,
778		Tcl_DStringValue(&utfDirString),
779		Tcl_DStringLength(&utfDirString), &dirString);
780    } else {
781	/*
782	 * NT 5.0 changed the meaning of lpstrInitialDir, so we have to ensure
783	 * that we set the [pwd] if the user didn't specify anything else.
784	 */
785
786	Tcl_DString cwd;
787
788	Tcl_DStringFree(&utfDirString);
789	if ((Tcl_GetCwd(interp, &utfDirString) == NULL) ||
790		(Tcl_TranslateFileName(interp,
791			Tcl_DStringValue(&utfDirString), &cwd) == NULL)) {
792	    Tcl_ResetResult(interp);
793	} else {
794	    Tcl_UtfToExternalDString(unicodeEncoding, Tcl_DStringValue(&cwd),
795		    Tcl_DStringLength(&cwd), &dirString);
796	}
797	Tcl_DStringFree(&cwd);
798    }
799    ofn.lpstrInitialDir = (WCHAR *) Tcl_DStringValue(&dirString);
800
801    if (title != NULL) {
802	Tcl_UtfToExternalDString(unicodeEncoding, title, -1, &titleString);
803	ofn.lpstrTitle = (WCHAR *) Tcl_DStringValue(&titleString);
804    }
805
806    /*
807     * Popup the dialog.
808     */
809
810    oldMode = Tcl_SetServiceMode(TCL_SERVICE_ALL);
811    if (open != 0) {
812	winCode = GetOpenFileNameW(&ofn);
813    } else {
814	winCode = GetSaveFileNameW(&ofn);
815    }
816    Tcl_SetServiceMode(oldMode);
817    EatSpuriousMessageBugFix();
818
819    /*
820     * Ensure that hWnd is enabled, because it can happen that we have updated
821     * the wrapper of the parent, which causes us to leave this child disabled
822     * (Windows loses sync).
823     */
824
825    EnableWindow(hWnd, 1);
826
827    /*
828     * Clear the interp result since anything may have happened during the
829     * modal loop.
830     */
831
832    Tcl_ResetResult(interp);
833
834    /*
835     * Process the results.
836     *
837     * Use the CommDlgExtendedError() function to retrieve the error code.
838     * This function can return one of about two dozen codes; most of these
839     * indicate some sort of gross system failure (insufficient memory, bad
840     * window handles, etc.). Most of the error codes will be ignored; as we
841     * find we want more specific error messages for particular errors, we can
842     * extend the code as needed.
843     */
844
845    cdlgerr = CommDlgExtendedError();
846
847    /*
848     * We now allow FNERR_BUFFERTOOSMALL when multiselection is enabled. The
849     * filename buffer has been dynamically allocated by the OFN dialog
850     * procedure to accomodate all selected files.
851     */
852
853    if ((winCode != 0)
854	    || ((cdlgerr == FNERR_BUFFERTOOSMALL)
855		    && (ofn.Flags & OFN_ALLOWMULTISELECT))) {
856	if (ofn.Flags & OFN_ALLOWMULTISELECT) {
857	    /*
858	     * The result in dynFileBuffer contains many items, separated by
859	     * NUL characters. It is terminated with two nulls in a row. The
860	     * first element is the directory path.
861	     */
862
863	    WCHAR *files = (WCHAR *) ofnData.dynFileBuffer;
864	    Tcl_Obj *returnList = Tcl_NewObj();
865	    int count = 0;
866
867	    /*
868	     * Get directory.
869	     */
870
871	    (void) ConvertExternalFilename(unicodeEncoding, (char *) files,
872		    &ds);
873
874	    while (*files != '\0') {
875		while (*files != '\0') {
876		    files++;
877		}
878		files++;
879		if (*files != '\0') {
880		    Tcl_Obj *fullnameObj;
881		    Tcl_DString filenameBuf;
882
883		    count++;
884		    (void) ConvertExternalFilename(unicodeEncoding,
885			    (char *) files, &filenameBuf);
886
887		    fullnameObj = Tcl_NewStringObj(Tcl_DStringValue(&ds),
888			    Tcl_DStringLength(&ds));
889		    Tcl_AppendToObj(fullnameObj, "/", -1);
890		    Tcl_AppendToObj(fullnameObj, Tcl_DStringValue(&filenameBuf),
891			    Tcl_DStringLength(&filenameBuf));
892		    Tcl_DStringFree(&filenameBuf);
893		    Tcl_ListObjAppendElement(NULL, returnList, fullnameObj);
894		}
895	    }
896
897	    if (count == 0) {
898		/*
899		 * Only one file was returned.
900		 */
901
902		Tcl_ListObjAppendElement(NULL, returnList,
903			Tcl_NewStringObj(Tcl_DStringValue(&ds),
904				Tcl_DStringLength(&ds)));
905	    }
906	    Tcl_SetObjResult(interp, returnList);
907	    Tcl_DStringFree(&ds);
908	} else {
909	    Tcl_AppendResult(interp, ConvertExternalFilename(unicodeEncoding,
910		    (char *) ofn.lpstrFile, &ds), NULL);
911	    Tcl_DStringFree(&ds);
912	}
913	result = TCL_OK;
914	if ((ofn.nFilterIndex > 0) &&
915		Tcl_GetCharLength(Tcl_GetObjResult(interp)) > 0 &&
916		typeVariableObj && filterObj) {
917	    int listObjc, count;
918	    Tcl_Obj **listObjv = NULL;
919	    Tcl_Obj **typeInfo = NULL;
920
921	    if (Tcl_ListObjGetElements(interp, filterObj,
922		    &listObjc, &listObjv) != TCL_OK) {
923		result = TCL_ERROR;
924	    } else if (Tcl_ListObjGetElements(interp,
925		    listObjv[ofn.nFilterIndex - 1], &count,
926		    &typeInfo) != TCL_OK) {
927		result = TCL_ERROR;
928	    } else if (Tcl_ObjSetVar2(interp, typeVariableObj, NULL,
929		    typeInfo[0], TCL_GLOBAL_ONLY|TCL_LEAVE_ERR_MSG) == NULL) {
930		result = TCL_ERROR;
931	    }
932	}
933    } else if (cdlgerr == FNERR_INVALIDFILENAME) {
934	Tcl_SetResult(interp, "invalid filename \"", TCL_STATIC);
935	Tcl_AppendResult(interp, ConvertExternalFilename(unicodeEncoding,
936		(char *) ofn.lpstrFile, &ds), "\"", NULL);
937	Tcl_DStringFree(&ds);
938    } else {
939	result = TCL_OK;
940    }
941
942    if (ofn.lpstrTitle != NULL) {
943	Tcl_DStringFree(&titleString);
944    }
945    if (ofn.lpstrInitialDir != NULL) {
946	Tcl_DStringFree(&dirString);
947    }
948    Tcl_DStringFree(&filterString);
949    if (ofn.lpstrDefExt != NULL) {
950	Tcl_DStringFree(&extString);
951    }
952
953  end:
954    Tcl_DStringFree(&utfDirString);
955    Tcl_DStringFree(&utfFilterString);
956    if (ofnData.dynFileBuffer != NULL) {
957	ckfree(ofnData.dynFileBuffer);
958	ofnData.dynFileBuffer = NULL;
959    }
960
961    return result;
962}
963
964/*
965 *-------------------------------------------------------------------------
966 *
967 * OFNHookProcW --
968 *
969 *	Dialog box hook function. This is used to sets the "tk_dialog"
970 *	variable for test/debugging when the dialog is ready to receive
971 *	messages. When multiple file selection is enabled this function
972 *	is used to process the list of names.
973 *
974 * Results:
975 *	Returns 0 to allow default processing of messages to occur.
976 *
977 * Side effects:
978 *	None.
979 *
980 *-------------------------------------------------------------------------
981 */
982
983static UINT APIENTRY
984OFNHookProcW(
985    HWND hdlg,			/* Handle to child dialog window. */
986    UINT uMsg,			/* Message identifier */
987    WPARAM wParam,		/* Message parameter */
988    LPARAM lParam)		/* Message parameter */
989{
990    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
991	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
992    OPENFILENAMEW *ofnPtr;
993    OFNData *ofnData;
994
995    if (uMsg == WM_INITDIALOG) {
996	TkWinSetUserData(hdlg, lParam);
997    } else if (uMsg == WM_NOTIFY) {
998	OFNOTIFYW *notifyPtr = (OFNOTIFYW *) lParam;
999
1000	/*
1001	 * This is weird... or not. The CDN_FILEOK is NOT sent when the selection
1002	 * exceeds declared buffer size (the nMaxFile member of the OPENFILENAMEW
1003	 * struct passed to GetOpenFileNameW function). So, we have to rely on
1004	 * the most recent CDN_SELCHANGE then. Unfortunately this means, that
1005	 * gathering the selected filenames happens twice when they fit into the
1006	 * declared buffer. Luckily, it's not frequent operation so it should
1007	 * not incur any noticeable delay. See [tktoolkit-Bugs-2987995]
1008	 */
1009	if (notifyPtr->hdr.code == CDN_FILEOK ||
1010		notifyPtr->hdr.code == CDN_SELCHANGE) {
1011	    int dirsize, selsize;
1012	    WCHAR *buffer;
1013	    int buffersize;
1014
1015	    /*
1016	     * Change of selection. Unscramble the unholy mess that's in the
1017	     * selection buffer, resizing it if necessary.
1018	     */
1019
1020	    ofnPtr = notifyPtr->lpOFN;
1021	    ofnData = (OFNData *) ofnPtr->lCustData;
1022	    buffer = (WCHAR *) ofnData->dynFileBuffer;
1023	    hdlg = GetParent(hdlg);
1024
1025	    selsize = SendMessageW(hdlg, CDM_GETSPEC, 0, 0);
1026	    dirsize = SendMessageW(hdlg, CDM_GETFOLDERPATH, 0, 0);
1027	    buffersize = (selsize + dirsize + 1) * 2;
1028
1029	    if (selsize > 1) {
1030		if (ofnData->dynFileBufferSize < buffersize) {
1031		    buffer = (WCHAR *) ckrealloc((char *) buffer, buffersize);
1032		    ofnData->dynFileBufferSize = buffersize;
1033		    ofnData->dynFileBuffer = (char *) buffer;
1034		}
1035
1036		SendMessageW(hdlg, CDM_GETFOLDERPATH, dirsize, (int) buffer);
1037		buffer += dirsize;
1038
1039		SendMessageW(hdlg, CDM_GETSPEC, selsize, (int) buffer);
1040
1041		/*
1042		 * If there are multiple files, delete the quotes and change
1043		 * every second quote to NULL terminator
1044		 */
1045
1046		if (buffer[0] == '"') {
1047		    BOOL findquote = TRUE;
1048		    WCHAR *tmp = buffer;
1049
1050		    while(*buffer != '\0') {
1051			if (findquote) {
1052			    if (*buffer == '"') {
1053				findquote = FALSE;
1054			    }
1055			    buffer++;
1056			} else {
1057			    if (*buffer == '"') {
1058				findquote = TRUE;
1059				*buffer = '\0';
1060			    }
1061			    *tmp++ = *buffer++;
1062			}
1063		    }
1064		    *tmp = '\0';		/* Second NULL terminator. */
1065		} else {
1066		    buffer[selsize] = '\0';	/* Second NULL terminator. */
1067
1068		    /*
1069		     * Replace directory terminating NULL with a backslash.
1070		     */
1071
1072		    buffer--;
1073		    *buffer = '\\';
1074		}
1075	    } else {
1076		/*
1077		 * Nothing is selected, so just empty the string.
1078		 */
1079
1080		if (buffer != NULL) {
1081		    *buffer = '\0';
1082		}
1083	    }
1084	}
1085    } else if (uMsg == WM_WINDOWPOSCHANGED) {
1086	/*
1087	 * This message is delivered at the right time to enable Tk to set the
1088	 * debug information. Unhooks itself so it won't set the debug
1089	 * information every time it gets a WM_WINDOWPOSCHANGED message.
1090	 */
1091
1092	ofnPtr = (OPENFILENAMEW *) TkWinGetUserData(hdlg);
1093	if (ofnPtr != NULL) {
1094	    ofnData = (OFNData *) ofnPtr->lCustData;
1095	    if (ofnData->interp != NULL) {
1096		hdlg = GetParent(hdlg);
1097		tsdPtr->debugInterp = ofnData->interp;
1098		Tcl_DoWhenIdle(SetTkDialog, hdlg);
1099	    }
1100	    TkWinSetUserData(hdlg, NULL);
1101	}
1102    }
1103    return 0;
1104}
1105
1106/*
1107 *----------------------------------------------------------------------
1108 *
1109 * GetFileNameA --
1110 *
1111 *	Calls GetOpenFileName() or GetSaveFileName().
1112 *
1113 * Results:
1114 *	See user documentation.
1115 *
1116 * Side effects:
1117 *	See user documentation.
1118 *
1119 *----------------------------------------------------------------------
1120 */
1121
1122static int
1123GetFileNameA(
1124    ClientData clientData,	/* Main window associated with interpreter. */
1125    Tcl_Interp *interp,		/* Current interpreter. */
1126    int objc,			/* Number of arguments. */
1127    Tcl_Obj *CONST objv[],	/* Argument objects. */
1128    int open)			/* 1 to call GetOpenFileName(), 0 to call
1129				 * GetSaveFileName(). */
1130{
1131    OPENFILENAME ofn;
1132    TCHAR file[TK_MULTI_MAX_PATH], savePath[MAX_PATH];
1133    OFNData ofnData;
1134    int cdlgerr;
1135    int filterIndex = 0, result = TCL_ERROR, winCode, oldMode, i, multi = 0;
1136    char *extension = NULL, *filter = NULL, *title = NULL;
1137    Tk_Window tkwin = (Tk_Window) clientData;
1138    HWND hWnd;
1139    Tcl_Obj *filterObj = NULL, *initialTypeObj = NULL, *typeVariableObj = NULL;
1140    Tcl_DString utfFilterString, utfDirString, ds;
1141    Tcl_DString extString, filterString, dirString, titleString;
1142    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
1143	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
1144    static CONST char *saveOptionStrings[] = {
1145	"-defaultextension", "-filetypes", "-initialdir", "-initialfile",
1146	"-parent", "-title", "-typevariable", NULL
1147    };
1148    static CONST char *openOptionStrings[] = {
1149	"-defaultextension", "-filetypes", "-initialdir", "-initialfile",
1150	"-multiple", "-parent", "-title", "-typevariable", NULL
1151    };
1152    CONST char **optionStrings;
1153
1154    enum options {
1155	FILE_DEFAULT,	FILE_TYPES,	FILE_INITDIR,	FILE_INITFILE,
1156	FILE_MULTIPLE,	FILE_PARENT,	FILE_TITLE, FILE_TYPEVARIABLE
1157    };
1158
1159    file[0] = '\0';
1160    ZeroMemory(&ofnData, sizeof(OFNData));
1161    Tcl_DStringInit(&utfFilterString);
1162    Tcl_DStringInit(&utfDirString);
1163
1164    /*
1165     * Parse the arguments.
1166     */
1167
1168    if (open) {
1169	optionStrings = openOptionStrings;
1170    } else {
1171	optionStrings = saveOptionStrings;
1172    }
1173
1174    for (i = 1; i < objc; i += 2) {
1175	int index;
1176	char *string;
1177	Tcl_Obj *optionPtr, *valuePtr;
1178
1179	optionPtr = objv[i];
1180	valuePtr = objv[i + 1];
1181
1182	if (Tcl_GetIndexFromObj(interp, optionPtr, optionStrings, "option", 0,
1183		&index) != TCL_OK) {
1184	    goto end;
1185	}
1186
1187	/*
1188	 * We want to maximize code sharing between the open and save file
1189	 * dialog implementations; in particular, the switch statement below.
1190	 * We use different sets of option strings from the GetIndexFromObj
1191	 * call above, but a single enumeration for both. The save file dialog
1192	 * doesn't support -multiple, but it falls in the middle of the
1193	 * enumeration. Ultimately, this means that when the index found by
1194	 * GetIndexFromObj is >= FILE_MULTIPLE, when doing a save file dialog,
1195	 * we have to increment the index, so that it matches the open file
1196	 * dialog enumeration.
1197	 */
1198
1199	if (!open && index >= FILE_MULTIPLE) {
1200	    index++;
1201	}
1202	if (i + 1 == objc) {
1203	    string = Tcl_GetString(optionPtr);
1204	    Tcl_AppendResult(interp, "value for \"", string, "\" missing",
1205		    NULL);
1206	    goto end;
1207	}
1208
1209	string = Tcl_GetString(valuePtr);
1210	switch ((enum options) index) {
1211	case FILE_DEFAULT:
1212	    if (string[0] == '.') {
1213		string++;
1214	    }
1215	    extension = string;
1216	    break;
1217	case FILE_TYPES:
1218	    filterObj = valuePtr;
1219	    break;
1220	case FILE_INITDIR:
1221	    Tcl_DStringFree(&utfDirString);
1222	    if (Tcl_TranslateFileName(interp, string, &utfDirString) == NULL) {
1223		goto end;
1224	    }
1225	    break;
1226	case FILE_INITFILE:
1227	    if (Tcl_TranslateFileName(interp, string, &ds) == NULL) {
1228		goto end;
1229	    }
1230	    Tcl_UtfToExternal(NULL, NULL, Tcl_DStringValue(&ds),
1231		    Tcl_DStringLength(&ds), 0, NULL, (char *) file,
1232		    sizeof(file), NULL, NULL, NULL);
1233	    Tcl_DStringFree(&ds);
1234	    break;
1235	case FILE_MULTIPLE:
1236	    if (Tcl_GetBooleanFromObj(interp, valuePtr, &multi) != TCL_OK) {
1237		return TCL_ERROR;
1238	    }
1239	    break;
1240	case FILE_PARENT:
1241	    tkwin = Tk_NameToWindow(interp, string, tkwin);
1242	    if (tkwin == NULL) {
1243		goto end;
1244	    }
1245	    break;
1246	case FILE_TITLE:
1247	    title = string;
1248	    break;
1249	case FILE_TYPEVARIABLE:
1250	    typeVariableObj = valuePtr;
1251	    initialTypeObj = Tcl_ObjGetVar2(interp, typeVariableObj, NULL,
1252		    TCL_GLOBAL_ONLY);
1253	    break;
1254	}
1255    }
1256
1257    if (MakeFilter(interp, filterObj, &utfFilterString, initialTypeObj,
1258	    &filterIndex) != TCL_OK) {
1259	goto end;
1260    }
1261    filter = Tcl_DStringValue(&utfFilterString);
1262
1263    Tk_MakeWindowExist(tkwin);
1264    hWnd = Tk_GetHWND(Tk_WindowId(tkwin));
1265
1266    ZeroMemory(&ofn, sizeof(OPENFILENAMEA));
1267    if (LOBYTE(LOWORD(GetVersion())) < 5) {
1268	ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400;
1269    } else {
1270	ofn.lStructSize = sizeof(ofn);
1271    }
1272    ofn.hwndOwner = hWnd;
1273    ofn.hInstance = TkWinGetHInstance(ofn.hwndOwner);
1274    ofn.lpstrFilter = NULL;
1275    ofn.lpstrCustomFilter = NULL;
1276    ofn.nMaxCustFilter = 0;
1277    ofn.lpstrFile = (LPTSTR) file;
1278    ofn.nMaxFile = TK_MULTI_MAX_PATH;
1279    ofn.lpstrFileTitle = NULL;
1280    ofn.nMaxFileTitle = 0;
1281    ofn.lpstrInitialDir = NULL;
1282    ofn.lpstrTitle = NULL;
1283    ofn.Flags = OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR
1284	    | OFN_EXPLORER | OFN_ENABLEHOOK;
1285    ofn.nFileOffset = 0;
1286    ofn.nFileExtension = 0;
1287    ofn.lpstrDefExt = NULL;
1288    ofn.lpfnHook = (LPOFNHOOKPROC) OFNHookProcA;
1289    ofn.lCustData = (LPARAM) &ofnData;
1290    ofn.lpTemplateName = NULL;
1291
1292    if (open != 0) {
1293	ofn.Flags |= OFN_FILEMUSTEXIST;
1294    } else {
1295	ofn.Flags |= OFN_OVERWRITEPROMPT;
1296    }
1297
1298    if (tsdPtr->debugFlag != 0) {
1299	ofnData.interp = interp;
1300    }
1301
1302    if (multi != 0) {
1303	ofn.Flags |= OFN_ALLOWMULTISELECT;
1304
1305	/*
1306	 * Starting buffer size. The buffer will be expanded by the OFN dialog
1307	 * procedure when necessary
1308	 */
1309
1310	ofnData.dynFileBufferSize = 1024;
1311	ofnData.dynFileBuffer = ckalloc(1024);
1312    }
1313
1314    if (extension != NULL) {
1315	Tcl_UtfToExternalDString(NULL, extension, -1, &extString);
1316	ofn.lpstrDefExt = (LPTSTR) Tcl_DStringValue(&extString);
1317    }
1318    Tcl_UtfToExternalDString(NULL, Tcl_DStringValue(&utfFilterString),
1319	    Tcl_DStringLength(&utfFilterString), &filterString);
1320    ofn.lpstrFilter = (LPTSTR) Tcl_DStringValue(&filterString);
1321    ofn.nFilterIndex = filterIndex;
1322
1323    if (Tcl_DStringValue(&utfDirString)[0] != '\0') {
1324	Tcl_UtfToExternalDString(NULL, Tcl_DStringValue(&utfDirString),
1325		Tcl_DStringLength(&utfDirString), &dirString);
1326    } else {
1327	/*
1328	 * NT 5.0 changed the meaning of lpstrInitialDir, so we have to ensure
1329	 * that we set the [pwd] if the user didn't specify anything else.
1330	 */
1331
1332	Tcl_DString cwd;
1333
1334	Tcl_DStringFree(&utfDirString);
1335	if ((Tcl_GetCwd(interp, &utfDirString) == NULL) ||
1336		(Tcl_TranslateFileName(interp,
1337			Tcl_DStringValue(&utfDirString), &cwd) == NULL)) {
1338	    Tcl_ResetResult(interp);
1339	} else {
1340	    Tcl_UtfToExternalDString(NULL, Tcl_DStringValue(&cwd),
1341		    Tcl_DStringLength(&cwd), &dirString);
1342	}
1343	Tcl_DStringFree(&cwd);
1344    }
1345    ofn.lpstrInitialDir = (LPTSTR) Tcl_DStringValue(&dirString);
1346
1347    if (title != NULL) {
1348	Tcl_UtfToExternalDString(NULL, title, -1, &titleString);
1349	ofn.lpstrTitle = (LPTSTR) Tcl_DStringValue(&titleString);
1350    }
1351
1352    /*
1353     * Popup the dialog.
1354     */
1355
1356    GetCurrentDirectory(MAX_PATH, savePath);
1357    oldMode = Tcl_SetServiceMode(TCL_SERVICE_ALL);
1358    if (open != 0) {
1359	winCode = GetOpenFileName(&ofn);
1360    } else {
1361	winCode = GetSaveFileName(&ofn);
1362    }
1363    Tcl_SetServiceMode(oldMode);
1364    EatSpuriousMessageBugFix();
1365    SetCurrentDirectory(savePath);
1366
1367    /*
1368     * Ensure that hWnd is enabled, because it can happen that we have updated
1369     * the wrapper of the parent, which causes us to leave this child disabled
1370     * (Windows loses sync).
1371     */
1372
1373    EnableWindow(hWnd, 1);
1374
1375    /*
1376     * Clear the interp result since anything may have happened during the
1377     * modal loop.
1378     */
1379
1380    Tcl_ResetResult(interp);
1381
1382    /*
1383     * Process the results.
1384     *
1385     * Use the CommDlgExtendedError() function to retrieve the error code.
1386     * This function can return one of about two dozen codes; most of these
1387     * indicate some sort of gross system failure (insufficient memory, bad
1388     * window handles, etc.) Most of the error codes will be ignored; as we
1389     * find we want specific error messages for particular errors, we can
1390     * extend the code as needed.
1391     */
1392
1393    cdlgerr = CommDlgExtendedError();
1394
1395    /*
1396     * We now allow FNERR_BUFFERTOOSMALL when multiselection is enabled. The
1397     * filename buffer has been dynamically allocated by the OFN dialog
1398     * procedure to accomodate all selected files.
1399     */
1400
1401    if ((winCode != 0)
1402	    || ((cdlgerr == FNERR_BUFFERTOOSMALL)
1403		    && (ofn.Flags & OFN_ALLOWMULTISELECT))) {
1404	if (ofn.Flags & OFN_ALLOWMULTISELECT) {
1405	    /*
1406	     * The result in dynFileBuffer contains many items, separated by
1407	     * NUL characters. It is terminated with two nulls in a row. The
1408	     * first element is the directory path (if multiple files are
1409	     * selected) or the only returned file (if only a single file has
1410	     * been chosen).
1411	     */
1412
1413	    char *files = ofnData.dynFileBuffer;
1414	    Tcl_Obj *returnList = Tcl_NewObj();
1415	    int count = 0;
1416
1417	    /*
1418	     * Get directory.
1419	     */
1420
1421	    (void) ConvertExternalFilename(NULL, (char *) files, &ds);
1422
1423	    while (*files != '\0') {
1424		while (*files != '\0') {
1425		    files++;
1426		}
1427		files++;
1428		if (*files != '\0') {
1429		    Tcl_Obj *fullnameObj;
1430		    Tcl_DString filename;
1431
1432		    count++;
1433		    (void) ConvertExternalFilename(NULL, (char *) files,
1434			    &filename);
1435		    fullnameObj = Tcl_NewStringObj(Tcl_DStringValue(&ds),
1436			    Tcl_DStringLength(&ds));
1437		    Tcl_AppendToObj(fullnameObj, "/", -1);
1438		    Tcl_AppendToObj(fullnameObj, Tcl_DStringValue(&filename),
1439			    Tcl_DStringLength(&filename));
1440		    Tcl_DStringFree(&filename);
1441		    Tcl_ListObjAppendElement(NULL, returnList, fullnameObj);
1442		}
1443	    }
1444	    if (count == 0) {
1445		/*
1446		 * Only one file was returned.
1447		 */
1448
1449		Tcl_ListObjAppendElement(NULL, returnList, Tcl_NewStringObj(
1450			Tcl_DStringValue(&ds), Tcl_DStringLength(&ds)));
1451	    }
1452	    Tcl_SetObjResult(interp, returnList);
1453	    Tcl_DStringFree(&ds);
1454	} else {
1455	    Tcl_AppendResult(interp, ConvertExternalFilename(NULL,
1456		    (char *) ofn.lpstrFile, &ds), NULL);
1457	    Tcl_DStringFree(&ds);
1458	}
1459	result = TCL_OK;
1460	if ((ofn.nFilterIndex > 0) &&
1461		(Tcl_GetCharLength(Tcl_GetObjResult(interp)) > 0) &&
1462		typeVariableObj && filterObj) {
1463	    int listObjc, count;
1464	    Tcl_Obj **listObjv = NULL;
1465	    Tcl_Obj **typeInfo = NULL;
1466
1467	    if (Tcl_ListObjGetElements(interp, filterObj, &listObjc,
1468		    &listObjv) != TCL_OK) {
1469		result = TCL_ERROR;
1470	    } else if (Tcl_ListObjGetElements(interp,
1471		    listObjv[ofn.nFilterIndex - 1], &count,
1472		    &typeInfo) != TCL_OK) {
1473		result = TCL_ERROR;
1474	    } else if (Tcl_ObjSetVar2(interp, typeVariableObj, NULL,
1475		    typeInfo[0], TCL_GLOBAL_ONLY|TCL_LEAVE_ERR_MSG) == NULL) {
1476		result = TCL_ERROR;
1477	    }
1478	}
1479    } else if (cdlgerr == FNERR_INVALIDFILENAME) {
1480	Tcl_SetResult(interp, "invalid filename \"", TCL_STATIC);
1481	Tcl_AppendResult(interp, ConvertExternalFilename(NULL,
1482		(char *) ofn.lpstrFile, &ds), "\"", NULL);
1483	Tcl_DStringFree(&ds);
1484    } else {
1485	result = TCL_OK;
1486    }
1487
1488    if (ofn.lpstrTitle != NULL) {
1489	Tcl_DStringFree(&titleString);
1490    }
1491    if (ofn.lpstrInitialDir != NULL) {
1492	Tcl_DStringFree(&dirString);
1493    }
1494    Tcl_DStringFree(&filterString);
1495    if (ofn.lpstrDefExt != NULL) {
1496	Tcl_DStringFree(&extString);
1497    }
1498
1499  end:
1500    Tcl_DStringFree(&utfDirString);
1501    Tcl_DStringFree(&utfFilterString);
1502    if (ofnData.dynFileBuffer != NULL) {
1503	ckfree(ofnData.dynFileBuffer);
1504	ofnData.dynFileBuffer = NULL;
1505    }
1506
1507    return result;
1508}
1509
1510/*
1511 *-------------------------------------------------------------------------
1512 *
1513 * OFNHookProcA --
1514 *
1515 *	Dialog box hook function. This is used to sets the "tk_dialog"
1516 *	variable for test/debugging when the dialog is ready to receive
1517 *	messages. When multiple file selection is enabled this function
1518 *	is used to process the list of names.
1519 *
1520 * Results:
1521 *	Returns 0 to allow default processing of messages to occur.
1522 *
1523 * Side effects:
1524 *	None.
1525 *
1526 *-------------------------------------------------------------------------
1527 */
1528
1529static UINT APIENTRY
1530OFNHookProcA(
1531    HWND hdlg,			/* handle to child dialog window */
1532    UINT uMsg,			/* message identifier */
1533    WPARAM wParam,		/* message parameter */
1534    LPARAM lParam)		/* message parameter */
1535{
1536    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
1537	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
1538    OPENFILENAME *ofnPtr;
1539    OFNData *ofnData;
1540
1541    if (uMsg == WM_INITDIALOG) {
1542	TkWinSetUserData(hdlg, lParam);
1543    } else if (uMsg == WM_NOTIFY) {
1544	OFNOTIFY *notifyPtr = (OFNOTIFY *) lParam;
1545
1546	/*
1547	 * This is weird... or not. The CDN_FILEOK is NOT sent when the selection
1548	 * exceeds declared buffer size (the nMaxFile member of the OPENFILENAMEW
1549	 * struct passed to GetOpenFileNameW function). So, we have to rely on
1550	 * the most recent CDN_SELCHANGE then. Unfortunately this means, that
1551	 * gathering the selected filenames happens twice when they fit into the
1552	 * declared buffer. Luckily, it's not frequent operation so it should
1553	 * not incur any noticeable delay. See [tktoolkit-Bugs-2987995]
1554	 */
1555	if (notifyPtr->hdr.code == CDN_FILEOK ||
1556		notifyPtr->hdr.code == CDN_SELCHANGE) {
1557	    int dirsize, selsize;
1558	    char *buffer;
1559	    int buffersize;
1560
1561	    /*
1562	     * Change of selection. Unscramble the unholy mess that's in the
1563	     * selection buffer, resizing it if necessary.
1564	     */
1565
1566	    ofnPtr = notifyPtr->lpOFN;
1567	    ofnData = (OFNData *) ofnPtr->lCustData;
1568	    buffer = ofnData->dynFileBuffer;
1569	    hdlg = GetParent(hdlg);
1570
1571	    selsize = SendMessage(hdlg, CDM_GETSPEC, 0, 0);
1572	    dirsize = SendMessage(hdlg, CDM_GETFOLDERPATH, 0, 0);
1573	    buffersize = selsize + dirsize + 1;
1574
1575	    if (selsize > 1) {
1576		if (ofnData->dynFileBufferSize < buffersize) {
1577		    buffer = ckrealloc(buffer, buffersize);
1578		    ofnData->dynFileBufferSize = buffersize;
1579		    ofnData->dynFileBuffer = buffer;
1580		}
1581
1582		SendMessage(hdlg, CDM_GETFOLDERPATH, dirsize, (int) buffer);
1583		buffer += dirsize;
1584		SendMessage(hdlg, CDM_GETSPEC, selsize, (int) buffer);
1585
1586		/*
1587		 * If there are multiple files, delete the quotes and change
1588		 * every second quote to NULL terminator.
1589		 */
1590
1591		if (buffer[0] == '"') {
1592		    BOOL findquote = TRUE;
1593		    char *tmp = buffer;
1594
1595		    while (*buffer != '\0') {
1596			if (findquote) {
1597			    if (*buffer == '"') {
1598				findquote = FALSE;
1599			    }
1600			    buffer++;
1601			} else {
1602			    if (*buffer == '"') {
1603				findquote = TRUE;
1604				*buffer = '\0';
1605			    }
1606			    *tmp++ = *buffer++;
1607			}
1608		    }
1609		    *tmp = '\0';		/* Second NULL terminator. */
1610		} else {
1611		    buffer[selsize] = '\0';	/* Second NULL terminator. */
1612
1613		    /*
1614		     * Replace directory terminating NULL with a backslash.
1615		     */
1616
1617		    buffer--;
1618		    *buffer = '\\';
1619		}
1620
1621	    } else {
1622		/*
1623		 * Nothing is selected, so just empty the string.
1624		 */
1625
1626		if (buffer != NULL) {
1627		    *buffer = '\0';
1628		}
1629	    }
1630	}
1631    } else if (uMsg == WM_WINDOWPOSCHANGED) {
1632	/*
1633	 * This message is delivered at the right time to both old-style and
1634	 * explorer-style hook procs to enable Tk to set the debug
1635	 * information. Unhooks itself so it won't set the debug information
1636	 * every time it gets a WM_WINDOWPOSCHANGED message.
1637	 */
1638
1639	ofnPtr = (OPENFILENAME *) TkWinGetUserData(hdlg);
1640	if (ofnPtr != NULL) {
1641	    ofnData = (OFNData *) ofnPtr->lCustData;
1642	    if (ofnData->interp != NULL) {
1643		if (ofnPtr->Flags & OFN_EXPLORER) {
1644		    hdlg = GetParent(hdlg);
1645		}
1646		tsdPtr->debugInterp = ofnData->interp;
1647		Tcl_DoWhenIdle(SetTkDialog, hdlg);
1648	    }
1649	    TkWinSetUserData(hdlg, NULL);
1650	}
1651    }
1652    return 0;
1653}
1654
1655/*
1656 *----------------------------------------------------------------------
1657 *
1658 * MakeFilter --
1659 *
1660 *	Allocate a buffer to store the filters in a format understood by
1661 *	Windows.
1662 *
1663 * Results:
1664 *	A standard TCL return value.
1665 *
1666 * Side effects:
1667 *	ofnPtr->lpstrFilter is modified.
1668 *
1669 *----------------------------------------------------------------------
1670 */
1671
1672static int
1673MakeFilter(
1674    Tcl_Interp *interp,		/* Current interpreter. */
1675    Tcl_Obj *valuePtr,		/* Value of the -filetypes option */
1676    Tcl_DString *dsPtr,		/* Filled with windows filter string. */
1677    Tcl_Obj *initialPtr,	/* Initial type name  */
1678    int *index)			/* Index of initial type in filter string */
1679{
1680    char *filterStr;
1681    char *p;
1682    char *initial = NULL;
1683    int pass;
1684    int ix = 0; /* index counter */
1685    FileFilterList flist;
1686    FileFilter *filterPtr;
1687
1688    if (initialPtr) {
1689	initial = Tcl_GetStringFromObj(initialPtr, NULL);
1690    }
1691    TkInitFileFilters(&flist);
1692    if (TkGetFileFilters(interp, &flist, valuePtr, 1) != TCL_OK) {
1693	return TCL_ERROR;
1694    }
1695
1696    if (flist.filters == NULL) {
1697	/*
1698	 * Use "All Files (*.*) as the default filter if none is specified
1699	 */
1700	char *defaultFilter = "All Files (*.*)";
1701
1702	p = filterStr = (char*)ckalloc(30 * sizeof(char));
1703
1704	strcpy(p, defaultFilter);
1705	p+= strlen(defaultFilter);
1706
1707	*p++ = '\0';
1708	*p++ = '*';
1709	*p++ = '.';
1710	*p++ = '*';
1711	*p++ = '\0';
1712	*p++ = '\0';
1713	*p = '\0';
1714
1715    } else {
1716	int len;
1717
1718	if (valuePtr == NULL) {
1719	    len = 0;
1720	} else {
1721	    (void) Tcl_GetStringFromObj(valuePtr, &len);
1722	}
1723
1724	/*
1725	 * We format the filetype into a string understood by Windows: {"Text
1726	 * Documents" {.doc .txt} {TEXT}} becomes "Text Documents
1727	 * (*.doc,*.txt)\0*.doc;*.txt\0"
1728	 *
1729	 * See the Windows OPENFILENAME manual page for details on the filter
1730	 * string format.
1731	 */
1732
1733	/*
1734	 * Since we may only add asterisks (*) to the filter, we need at most
1735	 * twice the size of the string to format the filter
1736	 */
1737
1738	filterStr = ckalloc((unsigned int) len * 3);
1739
1740	for (filterPtr = flist.filters, p = filterStr; filterPtr;
1741		filterPtr = filterPtr->next) {
1742	    char *sep;
1743	    FileFilterClause *clausePtr;
1744
1745	    /*
1746	     * Check initial index for match, set index. Filter index is 1
1747	     * based so increment first
1748	     */
1749	    ix++;
1750	    if (index && initial && (strcmp(initial, filterPtr->name) == 0)) {
1751		*index = ix;
1752	    }
1753
1754	    /*
1755	     * First, put in the name of the file type.
1756	     */
1757
1758	    strcpy(p, filterPtr->name);
1759	    p+= strlen(filterPtr->name);
1760	    *p++ = ' ';
1761	    *p++ = '(';
1762
1763	    for (pass = 1; pass <= 2; pass++) {
1764		/*
1765		 * In the first pass, we format the extensions in the name
1766		 * field. In the second pass, we format the extensions in the
1767		 * filter pattern field
1768		 */
1769
1770		sep = "";
1771		for (clausePtr=filterPtr->clauses;clausePtr;
1772			clausePtr=clausePtr->next) {
1773		    GlobPattern *globPtr;
1774
1775		    for (globPtr = clausePtr->patterns; globPtr;
1776			    globPtr = globPtr->next) {
1777			strcpy(p, sep);
1778			p += strlen(sep);
1779			strcpy(p, globPtr->pattern);
1780			p += strlen(globPtr->pattern);
1781
1782			if (pass == 1) {
1783			    sep = ",";
1784			} else {
1785			    sep = ";";
1786			}
1787		    }
1788		}
1789		if (pass == 1) {
1790		    *p ++ = ')';
1791		}
1792		*p++ = '\0';
1793	    }
1794	}
1795
1796	/*
1797	 * Windows requires the filter string to be ended by two NULL
1798	 * characters.
1799	 */
1800
1801	*p++ = '\0';
1802	*p = '\0';
1803    }
1804
1805    Tcl_DStringAppend(dsPtr, filterStr, (int) (p - filterStr));
1806    ckfree((char *) filterStr);
1807
1808    TkFreeFileFilters(&flist);
1809    return TCL_OK;
1810}
1811
1812/*
1813 *----------------------------------------------------------------------
1814 *
1815 * Tk_ChooseDirectoryObjCmd --
1816 *
1817 *	This function implements the "tk_chooseDirectory" dialog box for the
1818 *	Windows platform. See the user documentation for details on what it
1819 *	does. Uses the newer SHBrowseForFolder explorer type interface.
1820 *
1821 * Results:
1822 *	See user documentation.
1823 *
1824 * Side effects:
1825 *	A modal dialog window is created. Tcl_SetServiceMode() is called to
1826 *	allow background events to be processed
1827 *
1828 *----------------------------------------------------------------------
1829 *
1830 * The function tk_chooseDirectory pops up a dialog box for the user to select
1831 * a directory. The following option-value pairs are possible as command line
1832 * arguments:
1833 *
1834 * -initialdir dirname
1835 *
1836 * Specifies that the directories in directory should be displayed when the
1837 * dialog pops up. If this parameter is not specified, then the directories in
1838 * the current working directory are displayed. If the parameter specifies a
1839 * relative path, the return value will convert the relative path to an
1840 * absolute path. This option may not always work on the Macintosh. This is
1841 * not a bug. Rather, the General Controls control panel on the Mac allows the
1842 * end user to override the application default directory.
1843 *
1844 * -parent window
1845 *
1846 * Makes window the logical parent of the dialog. The dialog is displayed on
1847 * top of its parent window.
1848 *
1849 * -title titleString
1850 *
1851 * Specifies a string to display as the title of the dialog box. If this
1852 * option is not specified, then a default title will be displayed.
1853 *
1854 * -mustexist boolean
1855 *
1856 * Specifies whether the user may specify non-existant directories. If this
1857 * parameter is true, then the user may only select directories that already
1858 * exist. The default value is false.
1859 *
1860 * New Behaviour:
1861 *
1862 * - If mustexist = 0 and a user entered folder does not exist, a prompt will
1863 *   pop-up asking if the user wants another chance to change it. The old
1864 *   dialog just returned the bogus entry. On mustexist = 1, the entries MUST
1865 *   exist before exiting the box with OK.
1866 *
1867 *   Bugs:
1868 *
1869 * - If valid abs directory name is entered into the entry box and Enter
1870 *   pressed, the box will close returning the name. This is inconsistent when
1871 *   entering relative names or names with forward slashes, which are
1872 *   invalidated then corrected in the callback. After correction, the box is
1873 *   held open to allow further modification by the user.
1874 *
1875 * - Not sure how to implement localization of message prompts.
1876 *
1877 * - -title is really -message.
1878 *
1879 *----------------------------------------------------------------------
1880 */
1881
1882int
1883Tk_ChooseDirectoryObjCmd(
1884    ClientData clientData,	/* Main window associated with interpreter. */
1885    Tcl_Interp *interp,		/* Current interpreter. */
1886    int objc,			/* Number of arguments. */
1887    Tcl_Obj *CONST objv[])	/* Argument objects. */
1888{
1889    char path[MAX_PATH];
1890    int oldMode, result = TCL_ERROR, i;
1891    LPCITEMIDLIST pidl;		/* Returned by browser */
1892    BROWSEINFO bInfo;		/* Used by browser */
1893    CHOOSEDIRDATA cdCBData;	/* Structure to pass back and forth */
1894    LPMALLOC pMalloc;		/* Used by shell */
1895    Tk_Window tkwin = (Tk_Window) clientData;
1896    HWND hWnd;
1897    char *utfTitle = NULL;/* Title for window */
1898    TCHAR saveDir[MAX_PATH];
1899    Tcl_DString titleString;	/* UTF Title */
1900    Tcl_DString initDirString;	/* Initial directory */
1901    Tcl_Obj *objPtr;
1902    static CONST char *optionStrings[] = {
1903	"-initialdir", "-mustexist",  "-parent",  "-title", NULL
1904    };
1905    enum options {
1906	DIR_INITIAL,   DIR_EXIST,  DIR_PARENT, FILE_TITLE
1907    };
1908
1909    /*
1910     * Initialize
1911     */
1912
1913    path[0] = '\0';
1914    ZeroMemory(&cdCBData, sizeof(CHOOSEDIRDATA));
1915    cdCBData.interp = interp;
1916
1917    /*
1918     * Process the command line options
1919     */
1920
1921    for (i = 1; i < objc; i += 2) {
1922	int index;
1923	char *string;
1924	Tcl_Obj *optionPtr, *valuePtr;
1925
1926	optionPtr = objv[i];
1927	valuePtr = objv[i + 1];
1928
1929	if (Tcl_GetIndexFromObj(interp, optionPtr, optionStrings, "option", 0,
1930		&index) != TCL_OK) {
1931	    goto cleanup;
1932	}
1933	if (i + 1 == objc) {
1934	    string = Tcl_GetString(optionPtr);
1935	    Tcl_AppendResult(interp, "value for \"", string, "\" missing",
1936		    NULL);
1937	    goto cleanup;
1938	}
1939
1940	string = Tcl_GetString(valuePtr);
1941	switch ((enum options) index) {
1942	case DIR_INITIAL:
1943	    if (Tcl_TranslateFileName(interp,string,&initDirString) == NULL) {
1944		goto cleanup;
1945	    }
1946	    string = Tcl_DStringValue(&initDirString);
1947
1948	    /*
1949	     * Convert possible relative path to full path to keep dialog
1950	     * happy.
1951	     */
1952
1953	    GetFullPathName(string, MAX_PATH, saveDir, NULL);
1954	    lstrcpyn(cdCBData.utfInitDir, saveDir, MAX_PATH);
1955	    Tcl_DStringFree(&initDirString);
1956	    break;
1957	case DIR_EXIST:
1958	    if (Tcl_GetBooleanFromObj(interp, valuePtr,
1959		    &cdCBData.mustExist) != TCL_OK) {
1960		goto cleanup;
1961	    }
1962	    break;
1963	case DIR_PARENT:
1964	    tkwin = Tk_NameToWindow(interp, string, tkwin);
1965	    if (tkwin == NULL) {
1966		goto cleanup;
1967	    }
1968	    break;
1969	case FILE_TITLE:
1970	    utfTitle = string;
1971	    break;
1972	}
1973    }
1974
1975    /*
1976     * Get ready to call the browser
1977     */
1978
1979    Tk_MakeWindowExist(tkwin);
1980    hWnd = Tk_GetHWND(Tk_WindowId(tkwin));
1981
1982    /*
1983     * Setup the parameters used by SHBrowseForFolder
1984     */
1985
1986    bInfo.hwndOwner = hWnd;
1987    bInfo.pszDisplayName = path;
1988    bInfo.pidlRoot = NULL;
1989    if (lstrlen(cdCBData.utfInitDir) == 0) {
1990	GetCurrentDirectory(MAX_PATH, cdCBData.utfInitDir);
1991    }
1992    bInfo.lParam = (LPARAM) &cdCBData;
1993
1994    if (utfTitle != NULL) {
1995	Tcl_UtfToExternalDString(NULL, utfTitle, -1, &titleString);
1996	bInfo.lpszTitle = (LPTSTR) Tcl_DStringValue(&titleString);
1997    } else {
1998	bInfo.lpszTitle = "Please choose a directory, then select OK.";
1999    }
2000
2001    /*
2002     * Set flags to add edit box, status text line and use the new ui. Allow
2003     * override with magic variable (ignore errors in retrieval). See
2004     * http://msdn.microsoft.com/en-us/library/bb773205(VS.85).aspx for
2005     * possible flag values.
2006     */
2007
2008    bInfo.ulFlags = BIF_EDITBOX | BIF_STATUSTEXT | BIF_RETURNFSANCESTORS
2009	| BIF_VALIDATE | BIF_NEWDIALOGSTYLE;
2010    objPtr = Tcl_GetVar2Ex(interp, "::tk::winChooseDirFlags", NULL,
2011	    TCL_GLOBAL_ONLY);
2012    if (objPtr != NULL) {
2013	int flags;
2014	Tcl_GetIntFromObj(NULL, objPtr, &flags);
2015	bInfo.ulFlags = flags;
2016    }
2017
2018    /*
2019     * Callback to handle events
2020     */
2021
2022    bInfo.lpfn = (BFFCALLBACK) ChooseDirectoryValidateProc;
2023
2024    /*
2025     * Display dialog in background and process result. We look to give the
2026     * user a chance to change their mind on an invalid folder if mustexist is
2027     * 0.
2028     */
2029
2030    oldMode = Tcl_SetServiceMode(TCL_SERVICE_ALL);
2031    GetCurrentDirectory(MAX_PATH, saveDir);
2032    if (SHGetMalloc(&pMalloc) == NOERROR) {
2033	pidl = SHBrowseForFolder(&bInfo);
2034
2035	/*
2036	 * This is a fix for Windows 2000, which seems to modify the folder name
2037	 * buffer even when the dialog is canceled (in this case the buffer
2038	 * contains garbage). See [Bug #3002230]
2039	 */
2040	path[0] = '\0';
2041
2042	/*
2043	 * Null for cancel button or invalid dir, otherwise valid.
2044	 */
2045
2046	if (pidl != NULL) {
2047	    if (!SHGetPathFromIDList(pidl, path)) {
2048		Tcl_SetResult(interp, "Error: Not a file system folder\n",
2049			TCL_VOLATILE);
2050	    };
2051	    pMalloc->lpVtbl->Free(pMalloc, (void *) pidl);
2052	} else if (lstrlen(cdCBData.utfRetDir) > 0) {
2053	    lstrcpy(path, cdCBData.utfRetDir);
2054	}
2055	pMalloc->lpVtbl->Release(pMalloc);
2056    }
2057    SetCurrentDirectory(saveDir);
2058    Tcl_SetServiceMode(oldMode);
2059
2060    /*
2061     * Ensure that hWnd is enabled, because it can happen that we have updated
2062     * the wrapper of the parent, which causes us to leave this child disabled
2063     * (Windows loses sync).
2064     */
2065
2066    EnableWindow(hWnd, 1);
2067
2068    /*
2069     * Change the pathname to the Tcl "normalized" pathname, where back
2070     * slashes are used instead of forward slashes
2071     */
2072
2073    Tcl_ResetResult(interp);
2074    if (*path) {
2075	Tcl_DString ds;
2076
2077	Tcl_AppendResult(interp, ConvertExternalFilename(NULL, (char *) path,
2078		&ds), NULL);
2079	Tcl_DStringFree(&ds);
2080    }
2081
2082    result = TCL_OK;
2083
2084    if (utfTitle != NULL) {
2085	Tcl_DStringFree(&titleString);
2086    }
2087
2088  cleanup:
2089    return result;
2090}
2091
2092/*
2093 *----------------------------------------------------------------------
2094 *
2095 * ChooseDirectoryValidateProc --
2096 *
2097 *	Hook function called by the explorer ChooseDirectory dialog when
2098 *	events occur. It is used to validate the text entry the user may have
2099 *	entered.
2100 *
2101 * Results:
2102 *	Returns 0 to allow default processing of message, or 1 to tell default
2103 *	dialog function not to close.
2104 *
2105 *----------------------------------------------------------------------
2106 */
2107
2108static UINT APIENTRY
2109ChooseDirectoryValidateProc(
2110    HWND hwnd,
2111    UINT message,
2112    LPARAM lParam,
2113    LPARAM lpData)
2114{
2115    TCHAR selDir[MAX_PATH];
2116    CHOOSEDIRDATA *chooseDirSharedData = (CHOOSEDIRDATA *) lpData;
2117    Tcl_DString initDirString;
2118    char string[MAX_PATH];
2119    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
2120	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
2121
2122    if (tsdPtr->debugFlag) {
2123	tsdPtr->debugInterp = (Tcl_Interp *) chooseDirSharedData->interp;
2124	Tcl_DoWhenIdle(SetTkDialog, (ClientData) hwnd);
2125    }
2126    chooseDirSharedData->utfRetDir[0] = '\0';
2127    switch (message) {
2128    case BFFM_VALIDATEFAILED:
2129	/*
2130	 * First save and check to see if it is a valid path name, if so then
2131	 * make that path the one shown in the window. Otherwise, it failed
2132	 * the check and should be treated as such. Use
2133	 * Set/GetCurrentDirectory which allows relative path names and names
2134	 * with forward slashes. Use Tcl_TranslateFileName to make sure names
2135	 * like ~ are converted correctly.
2136	 */
2137
2138	if (Tcl_TranslateFileName(chooseDirSharedData->interp,
2139		(char *) lParam, &initDirString) == NULL) {
2140	    /*
2141	     * Should we expose the error (in the interp result) to the user
2142	     * at this point?
2143	     */
2144
2145	    chooseDirSharedData->utfRetDir[0] = '\0';
2146	    return 1;
2147	}
2148	lstrcpyn(string, Tcl_DStringValue(&initDirString), MAX_PATH);
2149	Tcl_DStringFree(&initDirString);
2150
2151	if (SetCurrentDirectory((char *)string) == 0) {
2152	    LPTSTR lpFilePart[MAX_PATH];
2153
2154	    /*
2155	     * Get the full path name to the user entry, at this point it does
2156	     * not exist so see if it is supposed to. Otherwise just return
2157	     * it.
2158	     */
2159
2160	    GetFullPathName(string, MAX_PATH,
2161		    chooseDirSharedData->utfRetDir, /*unused*/ lpFilePart);
2162	    if (chooseDirSharedData->mustExist) {
2163		/*
2164		 * User HAS to select a valid directory.
2165		 */
2166
2167		wsprintf(selDir, TEXT("Directory '%.200s' does not exist,\nplease select or enter an existing directory."), chooseDirSharedData->utfRetDir);
2168		MessageBox(NULL, selDir, NULL, MB_ICONEXCLAMATION|MB_OK);
2169		chooseDirSharedData->utfRetDir[0] = '\0';
2170		return 1;
2171	    }
2172	} else {
2173	    /*
2174	     * Changed to new folder OK, return immediatly with the current
2175	     * directory in utfRetDir.
2176	     */
2177
2178	    GetCurrentDirectory(MAX_PATH, chooseDirSharedData->utfRetDir);
2179	    return 0;
2180	}
2181	return 0;
2182
2183    case BFFM_SELCHANGED:
2184	/*
2185	 * Set the status window to the currently selected path and enable the
2186	 * OK button if a file system folder, otherwise disable the OK button
2187	 * for things like server names. Perhaps a new switch
2188	 * -enablenonfolders can be used to allow non folders to be selected.
2189	 *
2190	 * Not called when user changes edit box directly.
2191	 */
2192
2193	if (SHGetPathFromIDList((LPITEMIDLIST) lParam, selDir)) {
2194	    SendMessage(hwnd, BFFM_SETSTATUSTEXT, 0, (LPARAM) selDir);
2195	    // enable the OK button
2196	    SendMessage(hwnd, BFFM_ENABLEOK, 0, (LPARAM) 1);
2197	} else {
2198	    // disable the OK button
2199	    SendMessage(hwnd, BFFM_ENABLEOK, 0, (LPARAM) 0);
2200	}
2201	UpdateWindow(hwnd);
2202	return 1;
2203
2204    case BFFM_INITIALIZED: {
2205	/*
2206	 * Directory browser intializing - tell it where to start from, user
2207	 * specified parameter.
2208	 */
2209
2210	char *initDir = chooseDirSharedData->utfInitDir;
2211
2212	SetCurrentDirectory(initDir);
2213	if (*initDir == '\\') {
2214	    /*
2215	     * BFFM_SETSELECTION only understands UNC paths as pidls, so
2216	     * convert path to pidl using IShellFolder interface.
2217	     */
2218
2219	    LPMALLOC pMalloc;
2220	    LPSHELLFOLDER psfFolder;
2221
2222	    if (SUCCEEDED(SHGetMalloc(&pMalloc))) {
2223		if (SUCCEEDED(SHGetDesktopFolder(&psfFolder))) {
2224		    LPITEMIDLIST pidlMain;
2225		    ULONG ulCount, ulAttr;
2226		    Tcl_DString ds;
2227
2228		    Tcl_UtfToExternalDString(TkWinGetUnicodeEncoding(),
2229			    initDir, -1, &ds);
2230		    if (SUCCEEDED(psfFolder->lpVtbl->ParseDisplayName(
2231			    psfFolder, hwnd, NULL, (WCHAR *)
2232			    Tcl_DStringValue(&ds), &ulCount,&pidlMain,&ulAttr))
2233			    && (pidlMain != NULL)) {
2234			SendMessage(hwnd, BFFM_SETSELECTION, FALSE,
2235				(LPARAM) pidlMain);
2236			pMalloc->lpVtbl->Free(pMalloc, pidlMain);
2237		    }
2238		    psfFolder->lpVtbl->Release(psfFolder);
2239		    Tcl_DStringFree(&ds);
2240		}
2241		pMalloc->lpVtbl->Release(pMalloc);
2242	    }
2243	} else {
2244	    SendMessage(hwnd, BFFM_SETSELECTION, TRUE, (LPARAM) initDir);
2245	}
2246	SendMessage(hwnd, BFFM_ENABLEOK, 0, (LPARAM) 1);
2247	break;
2248    }
2249
2250    }
2251    return 0;
2252}
2253
2254/*
2255 *----------------------------------------------------------------------
2256 *
2257 * Tk_MessageBoxObjCmd --
2258 *
2259 *	This function implements the MessageBox window for the Windows
2260 *	platform. See the user documentation for details on what it does.
2261 *
2262 * Results:
2263 *	See user documentation.
2264 *
2265 * Side effects:
2266 *	None. The MessageBox window will be destroy before this function
2267 *	returns.
2268 *
2269 *----------------------------------------------------------------------
2270 */
2271
2272int
2273Tk_MessageBoxObjCmd(
2274    ClientData clientData,	/* Main window associated with interpreter. */
2275    Tcl_Interp *interp,		/* Current interpreter. */
2276    int objc,			/* Number of arguments. */
2277    Tcl_Obj *CONST objv[])	/* Argument objects. */
2278{
2279    Tk_Window tkwin = (Tk_Window) clientData, parent;
2280    HWND hWnd;
2281    Tcl_Obj *messageObj, *titleObj, *detailObj, *tmpObj;
2282    int defaultBtn, icon, type;
2283    int i, oldMode, winCode;
2284    UINT flags;
2285    static CONST char *optionStrings[] = {
2286	"-default",	"-detail",	"-icon",	"-message",
2287	"-parent",	"-title",	"-type",	NULL
2288    };
2289    enum options {
2290	MSG_DEFAULT,	MSG_DETAIL,	MSG_ICON,	MSG_MESSAGE,
2291	MSG_PARENT,	MSG_TITLE,	MSG_TYPE
2292    };
2293    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
2294	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
2295
2296    (void) TkWinGetUnicodeEncoding();
2297    defaultBtn = -1;
2298    detailObj = NULL;
2299    icon = MB_ICONINFORMATION;
2300    messageObj = NULL;
2301    parent = tkwin;
2302    titleObj = NULL;
2303    type = MB_OK;
2304
2305    for (i = 1; i < objc; i += 2) {
2306	int index;
2307	char *string;
2308	Tcl_Obj *optionPtr, *valuePtr;
2309
2310	optionPtr = objv[i];
2311	valuePtr = objv[i + 1];
2312
2313	if (Tcl_GetIndexFromObj(interp, optionPtr, optionStrings, "option",
2314		TCL_EXACT, &index) != TCL_OK) {
2315	    return TCL_ERROR;
2316	}
2317	if (i + 1 == objc) {
2318	    string = Tcl_GetString(optionPtr);
2319	    Tcl_AppendResult(interp, "value for \"", string, "\" missing",
2320		    NULL);
2321	    return TCL_ERROR;
2322	}
2323
2324	switch ((enum options) index) {
2325	case MSG_DEFAULT:
2326	    defaultBtn = TkFindStateNumObj(interp, optionPtr, buttonMap,
2327		    valuePtr);
2328	    if (defaultBtn < 0) {
2329		return TCL_ERROR;
2330	    }
2331	    break;
2332
2333	case MSG_DETAIL:
2334	    detailObj = valuePtr;
2335	    break;
2336
2337	case MSG_ICON:
2338	    icon = TkFindStateNumObj(interp, optionPtr, iconMap, valuePtr);
2339	    if (icon < 0) {
2340		return TCL_ERROR;
2341	    }
2342	    break;
2343
2344	case MSG_MESSAGE:
2345	    messageObj = valuePtr;
2346	    break;
2347
2348	case MSG_PARENT:
2349	    parent = Tk_NameToWindow(interp, Tcl_GetString(valuePtr), tkwin);
2350	    if (parent == NULL) {
2351		return TCL_ERROR;
2352	    }
2353	    break;
2354
2355	case MSG_TITLE:
2356	    titleObj = valuePtr;
2357	    break;
2358
2359	case MSG_TYPE:
2360	    type = TkFindStateNumObj(interp, optionPtr, typeMap, valuePtr);
2361	    if (type < 0) {
2362		return TCL_ERROR;
2363	    }
2364	    break;
2365	}
2366    }
2367
2368    Tk_MakeWindowExist(parent);
2369    hWnd = Tk_GetHWND(Tk_WindowId(parent));
2370
2371    flags = 0;
2372    if (defaultBtn >= 0) {
2373	int defaultBtnIdx = -1;
2374
2375	for (i = 0; i < (int) NUM_TYPES; i++) {
2376	    if (type == allowedTypes[i].type) {
2377		int j;
2378
2379		for (j = 0; j < 3; j++) {
2380		    if (allowedTypes[i].btnIds[j] == defaultBtn) {
2381			defaultBtnIdx = j;
2382			break;
2383		    }
2384		}
2385		if (defaultBtnIdx < 0) {
2386		    Tcl_AppendResult(interp, "invalid default button \"",
2387			    TkFindStateString(buttonMap, defaultBtn),
2388			    "\"", NULL);
2389		    return TCL_ERROR;
2390		}
2391		break;
2392	    }
2393	}
2394	flags = buttonFlagMap[defaultBtnIdx];
2395    }
2396
2397    flags |= icon | type | MB_SYSTEMMODAL;
2398
2399    tmpObj = messageObj ? Tcl_DuplicateObj(messageObj)
2400	    : Tcl_NewUnicodeObj(NULL, 0);
2401    Tcl_IncrRefCount(tmpObj);
2402    if (detailObj) {
2403	Tcl_AppendUnicodeToObj(tmpObj, L"\n\n", 2);
2404	Tcl_AppendObjToObj(tmpObj, detailObj);
2405    }
2406
2407    oldMode = Tcl_SetServiceMode(TCL_SERVICE_ALL);
2408
2409    /*
2410     * MessageBoxW exists for all platforms. Use it to allow unicode error
2411     * message to be displayed correctly where possible by the OS.
2412     *
2413     * In order to have the parent window icon reflected in a MessageBox, we
2414     * have to create a hook that will trigger when the MessageBox is being
2415     * created.
2416     */
2417
2418    tsdPtr->hSmallIcon = TkWinGetIcon(parent, ICON_SMALL);
2419    tsdPtr->hBigIcon   = TkWinGetIcon(parent, ICON_BIG);
2420    tsdPtr->hMsgBoxHook = SetWindowsHookEx(WH_CBT, MsgBoxCBTProc, NULL,
2421	    GetCurrentThreadId());
2422    winCode = MessageBoxW(hWnd, Tcl_GetUnicode(tmpObj),
2423	    titleObj ? Tcl_GetUnicode(titleObj) : L"", flags);
2424    UnhookWindowsHookEx(tsdPtr->hMsgBoxHook);
2425    (void) Tcl_SetServiceMode(oldMode);
2426
2427    /*
2428     * Ensure that hWnd is enabled, because it can happen that we have updated
2429     * the wrapper of the parent, which causes us to leave this child disabled
2430     * (Windows loses sync).
2431     */
2432
2433    EnableWindow(hWnd, 1);
2434
2435    Tcl_DecrRefCount(tmpObj);
2436
2437    Tcl_SetResult(interp, TkFindStateString(buttonMap, winCode), TCL_STATIC);
2438    return TCL_OK;
2439}
2440
2441static LRESULT CALLBACK
2442MsgBoxCBTProc(
2443    int nCode,
2444    WPARAM wParam,
2445    LPARAM lParam)
2446{
2447    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
2448	Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
2449
2450    if (nCode == HCBT_CREATEWND) {
2451	/*
2452	 * Window owned by our task is being created. Since the hook is
2453	 * installed just before the MessageBox call and removed after the
2454	 * MessageBox call, the window being created is either the message box
2455	 * or one of its controls. Check that the class is WC_DIALOG to ensure
2456	 * that it's the one we want.
2457	 */
2458
2459	LPCBT_CREATEWND lpcbtcreate = (LPCBT_CREATEWND) lParam;
2460
2461	if (WC_DIALOG == lpcbtcreate->lpcs->lpszClass) {
2462	    HWND hwnd = (HWND) wParam;
2463
2464	    SendMessage(hwnd, WM_SETICON, ICON_SMALL,
2465		    (LPARAM) tsdPtr->hSmallIcon);
2466	    SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM) tsdPtr->hBigIcon);
2467	}
2468    }
2469
2470    /*
2471     * Call the next hook proc, if there is one
2472     */
2473
2474    return CallNextHookEx(tsdPtr->hMsgBoxHook, nCode, wParam, lParam);
2475}
2476
2477/*
2478 * ----------------------------------------------------------------------
2479 *
2480 * SetTkDialog --
2481 *
2482 *	Records the HWND for a native dialog in the 'tk_dialog' variable so
2483 *	that the test-suite can operate on the correct dialog window. Use of
2484 *	this is enabled when a test program calls TkWinDialogDebug by calling
2485 *	the test command 'tkwinevent debug 1'.
2486 *
2487 * ----------------------------------------------------------------------
2488 */
2489
2490static void
2491SetTkDialog(
2492    ClientData clientData)
2493{
2494    ThreadSpecificData *tsdPtr = (ThreadSpecificData *)
2495	    Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData));
2496    char buf[32];
2497
2498    sprintf(buf, "0x%p", (HWND) clientData);
2499    Tcl_SetVar(tsdPtr->debugInterp, "tk_dialog", buf, TCL_GLOBAL_ONLY);
2500}
2501
2502/*
2503 * Factored out a common pattern in use in this file.
2504 */
2505static char *
2506ConvertExternalFilename(
2507    Tcl_Encoding encoding,
2508    char *filename,
2509    Tcl_DString *dsPtr)
2510{
2511    char *p;
2512
2513    Tcl_ExternalToUtfDString(encoding, filename, -1, dsPtr);
2514    for (p = Tcl_DStringValue(dsPtr); *p != '\0'; p++) {
2515	/*
2516	 * Change the pathname to the Tcl "normalized" pathname, where back
2517	 * slashes are used instead of forward slashes
2518	 */
2519
2520	if (*p == '\\') {
2521	    *p = '/';
2522	}
2523    }
2524    return Tcl_DStringValue(dsPtr);
2525}
2526
2527/*
2528 * Local Variables:
2529 * mode: c
2530 * c-basic-offset: 4
2531 * fill-column: 78
2532 * End:
2533 */
2534