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