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