1 /////////////////////////////////////////////////////////////////////////////
2 // Name:        src/msw/dirdlg.cpp
3 // Purpose:     wxDirDialog
4 // Author:      Julian Smart
5 // Modified by:
6 // Created:     01/02/97
7 // Copyright:   (c) Julian Smart
8 // Licence:     wxWindows licence
9 /////////////////////////////////////////////////////////////////////////////
10 
11 // ============================================================================
12 // declarations
13 // ============================================================================
14 
15 // ----------------------------------------------------------------------------
16 // headers
17 // ----------------------------------------------------------------------------
18 
19 // For compilers that support precompilation, includes "wx.h".
20 #include "wx/wxprec.h"
21 
22 #ifdef __BORLANDC__
23     #pragma hdrstop
24 #endif
25 
26 #if wxUSE_DIRDLG
27 
28 #if wxUSE_OLE && !defined(__GNUWIN32_OLD__) && (!defined(__WXWINCE__) || \
29     (defined(__HANDHELDPC__) && (_WIN32_WCE >= 500)))
30 
31 #include "wx/dirdlg.h"
32 #include "wx/modalhook.h"
33 
34 #ifndef WX_PRECOMP
35     #include "wx/utils.h"
36     #include "wx/dialog.h"
37     #include "wx/log.h"
38     #include "wx/app.h"     // for GetComCtl32Version()
39 #endif
40 
41 #include "wx/msw/private.h"
42 #include "wx/msw/wrapshl.h"
43 #include "wx/msw/private/comptr.h"
44 #include "wx/dynlib.h"
45 
46 #include <initguid.h>
47 
48 // We can only use IFileDialog under desktop Windows and we need
49 // wxDynamicLibrary for it.
50 #if wxUSE_DYNLIB_CLASS && !defined(__WXWINCE__)
51     #define wxUSE_IFILEDIALOG 1
52 #else
53     #define wxUSE_IFILEDIALOG 0
54 #endif
55 
56 #if wxUSE_IFILEDIALOG
57 // IFileDialog related declarations missing from some compilers headers.
58 
59 // IShellItem
60 #ifndef __IShellItem_INTERFACE_DEFINED__
61 
62 #ifndef SIGDN_FILESYSPATH
63     #define SIGDN_FILESYSPATH 0x80058000
64 #endif
65 
66 struct IShellItem : public IUnknown
67 {
68     virtual HRESULT wxSTDCALL BindToHandler(IBindCtx*, REFGUID, REFIID, void**) = 0;
69     virtual HRESULT wxSTDCALL GetParent(IShellItem**) = 0;
70     virtual HRESULT wxSTDCALL GetDisplayName(DWORD, LPWSTR*) = 0;
71     virtual HRESULT wxSTDCALL GetAttributes(ULONG, ULONG*) = 0;
72     virtual HRESULT wxSTDCALL Compare(IShellItem*, DWORD, int*) = 0;
73 };
74 
75 #endif // #ifndef __IShellItem_INTERFACE_DEFINED__
76 
77 // Define this GUID in any case, even when __IShellItem_INTERFACE_DEFINED__ is
78 // defined in the headers we might still not have it in the actual uuid.lib,
79 // this happens with at least VC7 used with its original (i.e. not updated) SDK
80 // and there is no harm in defining the GUID unconditionally.
81 DEFINE_GUID(IID_IShellItem,
82     0x43826D1E, 0xE718, 0x42EE, 0xBC, 0x55, 0xA1, 0xE2, 0x61, 0xC3, 0x7B, 0xFE);
83 
84 struct IShellItemFilter;
85 struct IFileDialogEvents;
86 
87 // IModalWindow
88 #ifndef __IModalWindow_INTERFACE_DEFINED__
89 
90 struct IModalWindow : public IUnknown
91 {
92     virtual HRESULT wxSTDCALL Show(HWND) = 0;
93 };
94 
95 #endif // #ifndef __IModalWindow_INTERFACE_DEFINED__
96 
97 // IFileDialog
98 #ifndef __IFileDialog_INTERFACE_DEFINED__
99 
100 #ifndef FOS_PICKFOLDERS
101     #define FOS_PICKFOLDERS 0x20
102 #endif
103 
104 #ifndef FOS_FORCEFILESYSTEM
105     #define FOS_FORCEFILESYSTEM 0x40
106 #endif
107 
108 struct _COMDLG_FILTERSPEC;
109 
110 struct IFileDialog : public IModalWindow
111 {
112     virtual HRESULT wxSTDCALL SetFileTypes(UINT, const _COMDLG_FILTERSPEC*) = 0;
113     virtual HRESULT wxSTDCALL SetFileTypeIndex(UINT) = 0;
114     virtual HRESULT wxSTDCALL GetFileTypeIndex(UINT*) = 0;
115     virtual HRESULT wxSTDCALL Advise(IFileDialogEvents*, DWORD*) = 0;
116     virtual HRESULT wxSTDCALL Unadvise(DWORD) = 0;
117     virtual HRESULT wxSTDCALL SetOptions(DWORD) = 0;
118     virtual HRESULT wxSTDCALL GetOptions(DWORD*) = 0;
119     virtual HRESULT wxSTDCALL SetDefaultFolder(IShellItem*) = 0;
120     virtual HRESULT wxSTDCALL SetFolder(IShellItem*) = 0;
121     virtual HRESULT wxSTDCALL GetFolder(IShellItem**) = 0;
122     virtual HRESULT wxSTDCALL GetCurrentSelection(IShellItem**) = 0;
123     virtual HRESULT wxSTDCALL SetFileName(LPCWSTR) = 0;
124     virtual HRESULT wxSTDCALL GetFileName(LPWSTR*) = 0;
125     virtual HRESULT wxSTDCALL SetTitle(LPCWSTR) = 0;
126     virtual HRESULT wxSTDCALL SetOkButtonLabel(LPCWSTR) = 0;
127     virtual HRESULT wxSTDCALL SetFileNameLabel(LPCWSTR) = 0;
128     virtual HRESULT wxSTDCALL GetResult(IShellItem**) = 0;
129     virtual HRESULT wxSTDCALL AddPlace(IShellItem*, DWORD) = 0;
130     virtual HRESULT wxSTDCALL SetDefaultExtension(LPCWSTR) = 0;
131     virtual HRESULT wxSTDCALL Close(HRESULT) = 0;
132     virtual HRESULT wxSTDCALL SetClientGuid(REFGUID) = 0;
133     virtual HRESULT wxSTDCALL ClearClientData() = 0;
134     virtual HRESULT wxSTDCALL SetFilter(IShellItemFilter*) = 0;
135 };
136 
137 DEFINE_GUID(CLSID_FileOpenDialog,
138     0xDC1C5A9C, 0xE88A, 0x4dde, 0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7);
139 
140 DEFINE_GUID(IID_IFileDialog,
141     0x42F85136, 0xDB7E, 0x439C, 0x85, 0xF1, 0xE4, 0x07, 0x5D, 0x13, 0x5F, 0xC8);
142 
143 #endif // #ifndef __IFileDialog_INTERFACE_DEFINED__
144 
145 #endif // wxUSE_IFILEDIALOG
146 
147 // ----------------------------------------------------------------------------
148 // constants
149 // ----------------------------------------------------------------------------
150 
151 #ifndef BIF_NEWDIALOGSTYLE
152     #define BIF_NEWDIALOGSTYLE 0x0040
153 #endif
154 
155 #ifndef BIF_NONEWFOLDERBUTTON
156     #define BIF_NONEWFOLDERBUTTON  0x0200
157 #endif
158 
159 #ifndef BIF_EDITBOX
160     #define BIF_EDITBOX 16
161 #endif
162 
163 // ----------------------------------------------------------------------------
164 // wxWidgets macros
165 // ----------------------------------------------------------------------------
166 
167 IMPLEMENT_CLASS(wxDirDialog, wxDialog)
168 
169 // ----------------------------------------------------------------------------
170 // private functions prototypes
171 // ----------------------------------------------------------------------------
172 
173 // the callback proc for the dir dlg
174 static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lp,
175                                        LPARAM pData);
176 
177 
178 // ============================================================================
179 // implementation
180 // ============================================================================
181 
182 // ----------------------------------------------------------------------------
183 // wxDirDialog
184 // ----------------------------------------------------------------------------
185 
wxDirDialog(wxWindow * parent,const wxString & message,const wxString & defaultPath,long style,const wxPoint & WXUNUSED (pos),const wxSize & WXUNUSED (size),const wxString & WXUNUSED (name))186 wxDirDialog::wxDirDialog(wxWindow *parent,
187                          const wxString& message,
188                          const wxString& defaultPath,
189                          long style,
190                          const wxPoint& WXUNUSED(pos),
191                          const wxSize& WXUNUSED(size),
192                          const wxString& WXUNUSED(name))
193 {
194     m_message = message;
195     m_parent = parent;
196 
197     SetWindowStyle(style);
198     SetPath(defaultPath);
199 }
200 
SetPath(const wxString & path)201 void wxDirDialog::SetPath(const wxString& path)
202 {
203     m_path = path;
204 
205     // SHBrowseForFolder doesn't like '/'s nor the trailing backslashes
206     m_path.Replace(wxT("/"), wxT("\\"));
207 
208     while ( !m_path.empty() && (*(m_path.end() - 1) == wxT('\\')) )
209     {
210         m_path.erase(m_path.length() - 1);
211     }
212 
213     // but the root drive should have a trailing slash (again, this is just
214     // the way the native dialog works)
215     if ( !m_path.empty() && (*(m_path.end() - 1) == wxT(':')) )
216     {
217         m_path += wxT('\\');
218     }
219 }
220 
ShowModal()221 int wxDirDialog::ShowModal()
222 {
223     WX_HOOK_MODAL_DIALOG();
224 
225     wxWindow* const parent = GetParentForModalDialog();
226     WXHWND hWndParent = parent ? GetHwndOf(parent) : NULL;
227 
228     // Use IFileDialog under new enough Windows, it's more user-friendly.
229     int rc;
230 #if wxUSE_IFILEDIALOG
231     // While the new dialog is available under Vista, it may return a wrong
232     // path there (see http://support.microsoft.com/kb/969885/en-us), so we
233     // don't use it there by default. We could improve the version test to
234     // allow its use if the comdlg32.dll version is greater than 6.0.6002.22125
235     // as this means that the hotfix correcting this bug is installed.
236     if ( wxGetWinVersion() > wxWinVersion_Vista )
237     {
238         rc = ShowIFileDialog(hWndParent);
239     }
240     else
241     {
242         rc = wxID_NONE;
243     }
244 
245     if ( rc == wxID_NONE )
246 #endif // wxUSE_IFILEDIALOG
247     {
248         rc = ShowSHBrowseForFolder(hWndParent);
249     }
250 
251     // change current working directory if asked so
252     if ( rc == wxID_OK && HasFlag(wxDD_CHANGE_DIR) )
253         wxSetWorkingDirectory(m_path);
254 
255     return rc;
256 }
257 
ShowSHBrowseForFolder(WXHWND owner)258 int wxDirDialog::ShowSHBrowseForFolder(WXHWND owner)
259 {
260     BROWSEINFO bi;
261     bi.hwndOwner      = owner;
262     bi.pidlRoot       = NULL;
263     bi.pszDisplayName = NULL;
264     // Please don't change this without checking it compiles
265     // with eVC++ first.
266 #if defined(__POCKETPC__) || defined(__SMARTPHONE__)
267     bi.lpszTitle      = m_message.mb_str();
268 #else
269     bi.lpszTitle      = m_message.c_str();
270 #endif
271     bi.ulFlags        = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT;
272     bi.lpfn           = BrowseCallbackProc;
273     bi.lParam         = wxMSW_CONV_LPARAM(m_path); // param for the callback
274 
275     static const int verComCtl32 = wxApp::GetComCtl32Version();
276 
277     // we always add the edit box (it doesn't hurt anybody, does it?) if it is
278     // supported by the system
279     if ( verComCtl32 >= 471 )
280     {
281         bi.ulFlags |= BIF_EDITBOX;
282     }
283 
284     // to have the "New Folder" button we must use the "new" dialog style which
285     // is also the only way to have a resizable dialog
286     //
287     // "new" style is only available in the version 5.0+ of comctl32.dll
288     const bool needNewDir = !HasFlag(wxDD_DIR_MUST_EXIST);
289     if ( (needNewDir || HasFlag(wxRESIZE_BORDER)) && (verComCtl32 >= 500) )
290     {
291         if (needNewDir)
292         {
293             bi.ulFlags |= BIF_NEWDIALOGSTYLE;
294         }
295         else
296         {
297             // Versions < 600 doesn't support BIF_NONEWFOLDERBUTTON
298             // The only way to get rid of the Make New Folder button is use
299             // the old dialog style which doesn't have the button thus we
300             // simply don't set the New Dialog Style for such comctl versions.
301             if (verComCtl32 >= 600)
302             {
303                 bi.ulFlags |= BIF_NEWDIALOGSTYLE;
304                 bi.ulFlags |= BIF_NONEWFOLDERBUTTON;
305             }
306         }
307     }
308 
309     // do show the dialog
310     wxItemIdList pidl(SHBrowseForFolder(&bi));
311 
312     wxItemIdList::Free((LPITEMIDLIST)bi.pidlRoot);
313 
314     if ( !pidl )
315     {
316         // Cancel button pressed
317         return wxID_CANCEL;
318     }
319 
320     m_path = pidl.GetPath();
321 
322     return m_path.empty() ? wxID_CANCEL : wxID_OK;
323 }
324 
325 // Function for obtaining folder name on Vista and newer.
326 //
327 // Returns wxID_OK on success, wxID_CANCEL if cancelled by user or wxID_NONE if
328 // an error occurred and we should fall back onto the old dialog.
329 #if wxUSE_IFILEDIALOG
330 
ShowIFileDialog(WXHWND owner)331 int wxDirDialog::ShowIFileDialog(WXHWND owner)
332 {
333     HRESULT hr;
334     wxCOMPtr<IFileDialog> fileDialog;
335 
336     hr = ::CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER,
337                             wxIID_PPV_ARGS(IFileDialog, &fileDialog));
338     if ( FAILED(hr) )
339     {
340         wxLogApiError(wxS("CoCreateInstance(CLSID_FileOpenDialog)"), hr);
341         return wxID_NONE;
342     }
343 
344     // allow user to select only a file system folder
345     hr = fileDialog->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
346     if ( FAILED(hr) )
347     {
348         wxLogApiError(wxS("IFileDialog::SetOptions"), hr);
349         return wxID_NONE;
350     }
351 
352     hr = fileDialog->SetTitle(m_message.wc_str());
353     if ( FAILED(hr) )
354     {
355         // This error is not serious, let's just log it and continue even
356         // without the title set.
357         wxLogApiError(wxS("IFileDialog::SetTitle"), hr);
358     }
359 
360     // set the initial path
361     if ( !m_path.empty() )
362     {
363         // We need to link SHCreateItemFromParsingName() dynamically as it's
364         // not available on pre-Vista systems.
365         typedef HRESULT
366         (WINAPI *SHCreateItemFromParsingName_t)(PCWSTR,
367                                                 IBindCtx*,
368                                                 REFIID,
369                                                 void**);
370 
371         SHCreateItemFromParsingName_t pfnSHCreateItemFromParsingName = NULL;
372         wxDynamicLibrary dllShell32;
373         if ( dllShell32.Load(wxS("shell32.dll"), wxDL_VERBATIM | wxDL_QUIET) )
374         {
375             wxDL_INIT_FUNC(pfn, SHCreateItemFromParsingName, dllShell32);
376         }
377 
378         if ( !pfnSHCreateItemFromParsingName )
379         {
380             wxLogLastError(wxS("SHCreateItemFromParsingName() not found"));
381             return wxID_NONE;
382         }
383 
384         wxCOMPtr<IShellItem> folder;
385         hr = pfnSHCreateItemFromParsingName(m_path.wc_str(),
386                                               NULL,
387                                               wxIID_PPV_ARGS(IShellItem,
388                                                              &folder));
389 
390         // Failing to parse the folder name is not really an error, we'll just
391         // ignore the initial directory in this case, but we should still show
392         // the dialog.
393         if ( FAILED(hr) )
394         {
395             if ( hr != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) )
396             {
397                 wxLogApiError(wxS("SHCreateItemFromParsingName"), hr);
398                 return wxID_NONE;
399             }
400         }
401         else // The folder was parsed correctly.
402         {
403             hr = fileDialog->SetFolder(folder);
404             if ( FAILED(hr) )
405             {
406                 wxLogApiError(wxS("IFileDialog::SetFolder"), hr);
407                 return wxID_NONE;
408             }
409         }
410     }
411 
412 
413     wxString path;
414 
415     hr = fileDialog->Show(owner);
416     if ( SUCCEEDED(hr) )
417     {
418         wxCOMPtr<IShellItem> folder;
419 
420         hr = fileDialog->GetResult(&folder);
421         if ( SUCCEEDED(hr) )
422         {
423             LPOLESTR pathOLE = NULL;
424 
425             hr = folder->GetDisplayName(SIGDN_FILESYSPATH, &pathOLE);
426             if ( SUCCEEDED(hr) )
427             {
428                 path = pathOLE;
429                 CoTaskMemFree(pathOLE);
430             }
431             else
432             {
433                 wxLogApiError(wxS("IShellItem::GetDisplayName"), hr);
434             }
435         }
436         else
437         {
438             wxLogApiError(wxS("IFileDialog::GetResult"), hr);
439         }
440     }
441     else if ( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
442     {
443         return wxID_CANCEL; // the user cancelled the dialog
444     }
445     else
446     {
447         wxLogApiError(wxS("IFileDialog::Show"), hr);
448     }
449 
450     if ( path.empty() )
451     {
452         // the user didn't cancel the dialog and yet the path is empty
453         // it means there was an error, already logged by wxLogApiError()
454         // now report the error to the user and return
455         wxLogSysError(_("Couldn't obtain folder name"), hr);
456         return wxID_CANCEL;
457     }
458 
459     m_path = path;
460     return wxID_OK;
461 }
462 
463 #endif // wxUSE_IFILEDIALOG
464 
465 // ----------------------------------------------------------------------------
466 // private functions
467 // ----------------------------------------------------------------------------
468 
469 static int CALLBACK
BrowseCallbackProc(HWND hwnd,UINT uMsg,LPARAM lp,LPARAM pData)470 BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lp, LPARAM pData)
471 {
472     switch(uMsg)
473     {
474 #ifdef BFFM_SETSELECTION
475         case BFFM_INITIALIZED:
476             // sent immediately after initialisation and so we may set the
477             // initial selection here
478             //
479             // wParam = TRUE => lParam is a string and not a PIDL
480             ::SendMessage(hwnd, BFFM_SETSELECTION, TRUE, pData);
481             break;
482 #endif // BFFM_SETSELECTION
483 
484 
485         case BFFM_SELCHANGED:
486             // note that this doesn't work with the new style UI (MSDN doesn't
487             // say anything about it, but the comments in shlobj.h do!) but we
488             // still execute this code in case it starts working again with the
489             // "new new UI" (or would it be "NewUIEx" according to tradition?)
490             {
491                 // Set the status window to the currently selected path.
492                 wxString strDir;
493                 if ( SHGetPathFromIDList((LPITEMIDLIST)lp,
494                                          wxStringBuffer(strDir, MAX_PATH)) )
495                 {
496                     // NB: this shouldn't be necessary with the new style box
497                     //     (which is resizable), but as for now it doesn't work
498                     //     anyhow (see the comment above) no harm in doing it
499 
500                     // need to truncate or it displays incorrectly
501                     static const size_t maxChars = 37;
502                     if ( strDir.length() > maxChars )
503                     {
504                         strDir = strDir.Right(maxChars);
505                         strDir = wxString(wxT("...")) + strDir;
506                     }
507 
508                     SendMessage(hwnd, BFFM_SETSTATUSTEXT,
509                                 0, wxMSW_CONV_LPARAM(strDir));
510                 }
511             }
512             break;
513 
514         //case BFFM_VALIDATEFAILED: -- might be used to provide custom message
515         //                             if the user types in invalid dir name
516     }
517 
518     return 0;
519 }
520 
521 #endif // compiler/platform on which the code here compiles
522 
523 #endif // wxUSE_DIRDLG
524