Bumping manifests a=b2g-bump
[gecko.git] / widget / windows / nsFilePicker.cpp
blobeb2e79c3dc6d9d997f1e023301dd1b804c86acf6
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsFilePicker.h"
9 #include <shlobj.h>
10 #include <shlwapi.h>
11 #include <cderr.h>
13 #include "mozilla/WindowsVersion.h"
14 #include "nsReadableUtils.h"
15 #include "nsNetUtil.h"
16 #include "nsWindow.h"
17 #include "nsILoadContext.h"
18 #include "nsIServiceManager.h"
19 #include "nsIURL.h"
20 #include "nsIStringBundle.h"
21 #include "nsEnumeratorUtils.h"
22 #include "nsCRT.h"
23 #include "nsString.h"
24 #include "nsToolkit.h"
25 #include "WinUtils.h"
26 #include "nsPIDOMWindow.h"
28 using mozilla::IsVistaOrLater;
29 using namespace mozilla::widget;
31 char16_t *nsFilePicker::mLastUsedUnicodeDirectory;
32 char nsFilePicker::mLastUsedDirectory[MAX_PATH+1] = { 0 };
34 static const wchar_t kDialogPtrProp[] = L"DialogPtrProperty";
35 static const DWORD kDialogTimerID = 9999;
36 static const unsigned long kDialogTimerTimeout = 300;
38 #define MAX_EXTENSION_LENGTH 10
39 #define FILE_BUFFER_SIZE 4096
41 typedef DWORD FILEOPENDIALOGOPTIONS;
43 ///////////////////////////////////////////////////////////////////////////////
44 // Helper classes
46 // Manages matching SuppressBlurEvents calls on the parent widget.
47 class AutoSuppressEvents
49 public:
50 explicit AutoSuppressEvents(nsIWidget* aWidget) :
51 mWindow(static_cast<nsWindow *>(aWidget)) {
52 SuppressWidgetEvents(true);
55 ~AutoSuppressEvents() {
56 SuppressWidgetEvents(false);
58 private:
59 void SuppressWidgetEvents(bool aFlag) {
60 if (mWindow) {
61 mWindow->SuppressBlurEvents(aFlag);
64 nsRefPtr<nsWindow> mWindow;
67 // Manages the current working path.
68 class AutoRestoreWorkingPath
70 public:
71 AutoRestoreWorkingPath() {
72 DWORD bufferLength = GetCurrentDirectoryW(0, nullptr);
73 mWorkingPath = new wchar_t[bufferLength];
74 if (GetCurrentDirectoryW(bufferLength, mWorkingPath) == 0) {
75 mWorkingPath = nullptr;
79 ~AutoRestoreWorkingPath() {
80 if (HasWorkingPath()) {
81 ::SetCurrentDirectoryW(mWorkingPath);
85 inline bool HasWorkingPath() const {
86 return mWorkingPath != nullptr;
88 private:
89 nsAutoArrayPtr<wchar_t> mWorkingPath;
92 // Manages NS_NATIVE_TMP_WINDOW child windows. NS_NATIVE_TMP_WINDOWs are
93 // temporary child windows of mParentWidget created to address RTL issues
94 // in picker dialogs. We are responsible for destroying these.
95 class AutoDestroyTmpWindow
97 public:
98 explicit AutoDestroyTmpWindow(HWND aTmpWnd) :
99 mWnd(aTmpWnd) {
102 ~AutoDestroyTmpWindow() {
103 if (mWnd)
104 DestroyWindow(mWnd);
107 inline HWND get() const { return mWnd; }
108 private:
109 HWND mWnd;
112 // Manages matching PickerOpen/PickerClosed calls on the parent widget.
113 class AutoWidgetPickerState
115 public:
116 explicit AutoWidgetPickerState(nsIWidget* aWidget) :
117 mWindow(static_cast<nsWindow *>(aWidget)) {
118 PickerState(true);
121 ~AutoWidgetPickerState() {
122 PickerState(false);
124 private:
125 void PickerState(bool aFlag) {
126 if (mWindow) {
127 if (aFlag)
128 mWindow->PickerOpen();
129 else
130 mWindow->PickerClosed();
133 nsRefPtr<nsWindow> mWindow;
136 // Manages a simple callback timer
137 class AutoTimerCallbackCancel
139 public:
140 AutoTimerCallbackCancel(nsFilePicker* aTarget,
141 nsTimerCallbackFunc aCallbackFunc) {
142 Init(aTarget, aCallbackFunc);
145 ~AutoTimerCallbackCancel() {
146 if (mPickerCallbackTimer) {
147 mPickerCallbackTimer->Cancel();
151 private:
152 void Init(nsFilePicker* aTarget,
153 nsTimerCallbackFunc aCallbackFunc) {
154 mPickerCallbackTimer = do_CreateInstance("@mozilla.org/timer;1");
155 if (!mPickerCallbackTimer) {
156 NS_WARNING("do_CreateInstance for timer failed??");
157 return;
159 mPickerCallbackTimer->InitWithFuncCallback(aCallbackFunc,
160 aTarget,
161 kDialogTimerTimeout,
162 nsITimer::TYPE_REPEATING_SLACK);
164 nsCOMPtr<nsITimer> mPickerCallbackTimer;
168 ///////////////////////////////////////////////////////////////////////////////
169 // nsIFilePicker
171 nsFilePicker::nsFilePicker() :
172 mSelectedType(1)
173 , mDlgWnd(nullptr)
174 , mFDECookie(0)
176 CoInitialize(nullptr);
179 nsFilePicker::~nsFilePicker()
181 if (mLastUsedUnicodeDirectory) {
182 NS_Free(mLastUsedUnicodeDirectory);
183 mLastUsedUnicodeDirectory = nullptr;
185 CoUninitialize();
188 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
190 NS_IMETHODIMP nsFilePicker::Init(nsIDOMWindow *aParent, const nsAString& aTitle, int16_t aMode)
192 nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aParent);
193 nsIDocShell* docShell = window ? window->GetDocShell() : nullptr;
194 mLoadContext = do_QueryInterface(docShell);
196 return nsBaseFilePicker::Init(aParent, aTitle, aMode);
199 STDMETHODIMP nsFilePicker::QueryInterface(REFIID refiid, void** ppvResult)
201 *ppvResult = nullptr;
202 if (IID_IUnknown == refiid ||
203 refiid == IID_IFileDialogEvents) {
204 *ppvResult = this;
207 if (nullptr != *ppvResult) {
208 ((LPUNKNOWN)*ppvResult)->AddRef();
209 return S_OK;
212 return E_NOINTERFACE;
216 * XP picker callbacks
219 // Show - Display the file dialog
220 int CALLBACK
221 BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
223 if (uMsg == BFFM_INITIALIZED)
225 char16_t * filePath = (char16_t *) lpData;
226 if (filePath)
227 ::SendMessageW(hwnd, BFFM_SETSELECTIONW,
228 TRUE /* true because lpData is a path string */,
229 lpData);
231 return 0;
234 static void
235 EnsureWindowVisible(HWND hwnd)
237 // Obtain the monitor which has the largest area of intersection
238 // with the window, or nullptr if there is no intersection.
239 HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL);
240 if (!monitor) {
241 // The window is not visible, we should reposition it to the same place as its parent
242 HWND parentHwnd = GetParent(hwnd);
243 RECT parentRect;
244 GetWindowRect(parentHwnd, &parentRect);
245 SetWindowPos(hwnd, nullptr, parentRect.left, parentRect.top, 0, 0,
246 SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER);
250 // Callback hook which will ensure that the window is visible. Currently
251 // only in use on os <= XP.
252 UINT_PTR CALLBACK
253 nsFilePicker::FilePickerHook(HWND hwnd,
254 UINT msg,
255 WPARAM wParam,
256 LPARAM lParam)
258 switch(msg) {
259 case WM_NOTIFY:
261 LPOFNOTIFYW lpofn = (LPOFNOTIFYW) lParam;
262 if (!lpofn || !lpofn->lpOFN) {
263 return 0;
266 if (CDN_INITDONE == lpofn->hdr.code) {
267 // The Window will be automatically moved to the last position after
268 // CDN_INITDONE. We post a message to ensure the window will be visible
269 // so it will be done after the automatic last position window move.
270 PostMessage(hwnd, MOZ_WM_ENSUREVISIBLE, 0, 0);
273 break;
274 case MOZ_WM_ENSUREVISIBLE:
275 EnsureWindowVisible(GetParent(hwnd));
276 break;
277 case WM_INITDIALOG:
279 OPENFILENAMEW* pofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
280 SetProp(hwnd, kDialogPtrProp, (HANDLE)pofn->lCustData);
281 nsFilePicker* picker = reinterpret_cast<nsFilePicker*>(pofn->lCustData);
282 if (picker) {
283 picker->SetDialogHandle(hwnd);
284 SetTimer(hwnd, kDialogTimerID, kDialogTimerTimeout, nullptr);
287 break;
288 case WM_TIMER:
290 // Check to see if our parent has been torn down, if so, we close too.
291 if (wParam == kDialogTimerID) {
292 nsFilePicker* picker =
293 reinterpret_cast<nsFilePicker*>(GetProp(hwnd, kDialogPtrProp));
294 if (picker && picker->ClosePickerIfNeeded(true)) {
295 KillTimer(hwnd, kDialogTimerID);
299 break;
301 return 0;
305 // Callback hook which will dynamically allocate a buffer large enough
306 // for the file picker dialog. Currently only in use on os <= XP.
307 UINT_PTR CALLBACK
308 nsFilePicker::MultiFilePickerHook(HWND hwnd,
309 UINT msg,
310 WPARAM wParam,
311 LPARAM lParam)
313 switch (msg) {
314 case WM_INITDIALOG:
316 // Finds the child drop down of a File Picker dialog and sets the
317 // maximum amount of text it can hold when typed in manually.
318 // A wParam of 0 mean 0x7FFFFFFE characters.
319 HWND comboBox = FindWindowEx(GetParent(hwnd), nullptr,
320 L"ComboBoxEx32", nullptr );
321 if(comboBox)
322 SendMessage(comboBox, CB_LIMITTEXT, 0, 0);
323 // Store our nsFilePicker ptr for future use
324 OPENFILENAMEW* pofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
325 SetProp(hwnd, kDialogPtrProp, (HANDLE)pofn->lCustData);
326 nsFilePicker* picker =
327 reinterpret_cast<nsFilePicker*>(pofn->lCustData);
328 if (picker) {
329 picker->SetDialogHandle(hwnd);
330 SetTimer(hwnd, kDialogTimerID, kDialogTimerTimeout, nullptr);
333 break;
334 case WM_NOTIFY:
336 LPOFNOTIFYW lpofn = (LPOFNOTIFYW) lParam;
337 if (!lpofn || !lpofn->lpOFN) {
338 return 0;
340 // CDN_SELCHANGE is sent when the selection in the list box of the file
341 // selection dialog changes
342 if (lpofn->hdr.code == CDN_SELCHANGE) {
343 HWND parentHWND = GetParent(hwnd);
345 // Get the required size for the selected files buffer
346 UINT newBufLength = 0;
347 int requiredBufLength = CommDlg_OpenSave_GetSpecW(parentHWND,
348 nullptr, 0);
349 if(requiredBufLength >= 0)
350 newBufLength += requiredBufLength;
351 else
352 newBufLength += MAX_PATH;
354 // If the user selects multiple files, the buffer contains the
355 // current directory followed by the file names of the selected
356 // files. So make room for the directory path. If the user
357 // selects a single file, it is no harm to add extra space.
358 requiredBufLength = CommDlg_OpenSave_GetFolderPathW(parentHWND,
359 nullptr, 0);
360 if(requiredBufLength >= 0)
361 newBufLength += requiredBufLength;
362 else
363 newBufLength += MAX_PATH;
365 // Check if lpstrFile and nMaxFile are large enough
366 if (newBufLength > lpofn->lpOFN->nMaxFile) {
367 if (lpofn->lpOFN->lpstrFile)
368 delete[] lpofn->lpOFN->lpstrFile;
370 // We allocate FILE_BUFFER_SIZE more bytes than is needed so that
371 // if the user selects a file and holds down shift and down to
372 // select additional items, we will not continuously reallocate
373 newBufLength += FILE_BUFFER_SIZE;
375 wchar_t* filesBuffer = new wchar_t[newBufLength];
376 ZeroMemory(filesBuffer, newBufLength * sizeof(wchar_t));
378 lpofn->lpOFN->lpstrFile = filesBuffer;
379 lpofn->lpOFN->nMaxFile = newBufLength;
383 break;
384 case WM_TIMER:
386 // Check to see if our parent has been torn down, if so, we close too.
387 if (wParam == kDialogTimerID) {
388 nsFilePicker* picker =
389 reinterpret_cast<nsFilePicker*>(GetProp(hwnd, kDialogPtrProp));
390 if (picker && picker->ClosePickerIfNeeded(true)) {
391 KillTimer(hwnd, kDialogTimerID);
395 break;
398 return FilePickerHook(hwnd, msg, wParam, lParam);
402 * Vista+ callbacks
405 HRESULT
406 nsFilePicker::OnFileOk(IFileDialog *pfd)
408 return S_OK;
411 HRESULT
412 nsFilePicker::OnFolderChanging(IFileDialog *pfd,
413 IShellItem *psiFolder)
415 return S_OK;
418 HRESULT
419 nsFilePicker::OnFolderChange(IFileDialog *pfd)
421 return S_OK;
424 HRESULT
425 nsFilePicker::OnSelectionChange(IFileDialog *pfd)
427 return S_OK;
430 HRESULT
431 nsFilePicker::OnShareViolation(IFileDialog *pfd,
432 IShellItem *psi,
433 FDE_SHAREVIOLATION_RESPONSE *pResponse)
435 return S_OK;
438 HRESULT
439 nsFilePicker::OnTypeChange(IFileDialog *pfd)
441 // Failures here result in errors due to security concerns.
442 nsRefPtr<IOleWindow> win;
443 pfd->QueryInterface(IID_IOleWindow, getter_AddRefs(win));
444 if (!win) {
445 NS_ERROR("Could not retrieve the IOleWindow interface for IFileDialog.");
446 return S_OK;
448 HWND hwnd = nullptr;
449 win->GetWindow(&hwnd);
450 if (!hwnd) {
451 NS_ERROR("Could not retrieve the HWND for IFileDialog.");
452 return S_OK;
455 SetDialogHandle(hwnd);
456 return S_OK;
459 HRESULT
460 nsFilePicker::OnOverwrite(IFileDialog *pfd,
461 IShellItem *psi,
462 FDE_OVERWRITE_RESPONSE *pResponse)
464 return S_OK;
468 * Close on parent close logic
471 bool
472 nsFilePicker::ClosePickerIfNeeded(bool aIsXPDialog)
474 if (!mParentWidget || !mDlgWnd)
475 return false;
477 nsWindow *win = static_cast<nsWindow *>(mParentWidget.get());
478 // Note, the xp callbacks hand us an inner window, so we have to step up
479 // one to get the actual dialog.
480 HWND dlgWnd;
481 if (aIsXPDialog)
482 dlgWnd = GetParent(mDlgWnd);
483 else
484 dlgWnd = mDlgWnd;
485 if (IsWindow(dlgWnd) && IsWindowVisible(dlgWnd) && win->DestroyCalled()) {
486 wchar_t className[64];
487 // Make sure we have the right window
488 if (GetClassNameW(dlgWnd, className, mozilla::ArrayLength(className)) &&
489 !wcscmp(className, L"#32770") &&
490 DestroyWindow(dlgWnd)) {
491 mDlgWnd = nullptr;
492 return true;
495 return false;
498 void
499 nsFilePicker::PickerCallbackTimerFunc(nsITimer *aTimer, void *aCtx)
501 nsFilePicker* picker = (nsFilePicker*)aCtx;
502 if (picker->ClosePickerIfNeeded(false)) {
503 aTimer->Cancel();
507 void
508 nsFilePicker::SetDialogHandle(HWND aWnd)
510 if (!aWnd || mDlgWnd)
511 return;
512 mDlgWnd = aWnd;
516 * Folder picker invocation
519 // Open the older XP style folder picker dialog. We end up in this call
520 // on XP systems or when platform is built without the longhorn SDK.
521 bool
522 nsFilePicker::ShowXPFolderPicker(const nsString& aInitialDir)
524 bool result = false;
526 nsAutoArrayPtr<wchar_t> dirBuffer(new wchar_t[FILE_BUFFER_SIZE]);
527 wcsncpy(dirBuffer, aInitialDir.get(), FILE_BUFFER_SIZE);
528 dirBuffer[FILE_BUFFER_SIZE-1] = '\0';
530 AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ?
531 mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr));
533 BROWSEINFOW browserInfo = {0};
534 browserInfo.pidlRoot = nullptr;
535 browserInfo.pszDisplayName = dirBuffer;
536 browserInfo.lpszTitle = mTitle.get();
537 browserInfo.ulFlags = BIF_USENEWUI | BIF_RETURNONLYFSDIRS;
538 browserInfo.hwndOwner = adtw.get();
539 browserInfo.iImage = 0;
540 browserInfo.lParam = reinterpret_cast<LPARAM>(this);
542 if (!aInitialDir.IsEmpty()) {
543 // the dialog is modal so that |initialDir.get()| will be valid in
544 // BrowserCallbackProc. Thus, we don't need to clone it.
545 browserInfo.lParam = (LPARAM) aInitialDir.get();
546 browserInfo.lpfn = &BrowseCallbackProc;
547 } else {
548 browserInfo.lParam = 0;
549 browserInfo.lpfn = nullptr;
552 LPITEMIDLIST list = ::SHBrowseForFolderW(&browserInfo);
553 if (list) {
554 result = ::SHGetPathFromIDListW(list, dirBuffer);
555 if (result)
556 mUnicodeFile.Assign(static_cast<const wchar_t*>(dirBuffer));
557 // free PIDL
558 CoTaskMemFree(list);
561 return result;
565 * Show a folder picker post Windows XP
567 * @param aInitialDir The initial directory, the last used directory will be
568 * used if left blank.
569 * @param aWasInitError Out parameter will hold true if there was an error
570 * before the folder picker is shown.
571 * @return true if a file was selected successfully.
573 bool
574 nsFilePicker::ShowFolderPicker(const nsString& aInitialDir, bool &aWasInitError)
576 nsRefPtr<IFileOpenDialog> dialog;
577 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC,
578 IID_IFileOpenDialog,
579 getter_AddRefs(dialog)))) {
580 aWasInitError = true;
581 return false;
583 aWasInitError = false;
585 // hook up event callbacks
586 dialog->Advise(this, &mFDECookie);
588 // options
589 FILEOPENDIALOGOPTIONS fos = FOS_PICKFOLDERS;
590 dialog->SetOptions(fos);
592 // initial strings
593 dialog->SetTitle(mTitle.get());
594 if (!aInitialDir.IsEmpty()) {
595 nsRefPtr<IShellItem> folder;
596 if (SUCCEEDED(
597 WinUtils::SHCreateItemFromParsingName(aInitialDir.get(), nullptr,
598 IID_IShellItem,
599 getter_AddRefs(folder)))) {
600 dialog->SetFolder(folder);
604 AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ?
605 mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr));
607 // display
608 nsRefPtr<IShellItem> item;
609 if (FAILED(dialog->Show(adtw.get())) ||
610 FAILED(dialog->GetResult(getter_AddRefs(item))) ||
611 !item) {
612 dialog->Unadvise(mFDECookie);
613 return false;
615 dialog->Unadvise(mFDECookie);
617 // results
619 // If the user chose a Win7 Library, resolve to the library's
620 // default save folder.
621 nsRefPtr<IShellItem> folderPath;
622 nsRefPtr<IShellLibrary> shellLib;
623 CoCreateInstance(CLSID_ShellLibrary, nullptr, CLSCTX_INPROC,
624 IID_IShellLibrary, getter_AddRefs(shellLib));
625 if (shellLib &&
626 SUCCEEDED(shellLib->LoadLibraryFromItem(item, STGM_READ)) &&
627 SUCCEEDED(shellLib->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem,
628 getter_AddRefs(folderPath)))) {
629 item.swap(folderPath);
632 // get the folder's file system path
633 return WinUtils::GetShellItemPath(item, mUnicodeFile);
637 * File open and save picker invocation
640 bool
641 nsFilePicker::FilePickerWrapper(OPENFILENAMEW* ofn, PickerType aType)
643 if (!ofn)
644 return false;
646 bool result = false;
647 AutoWidgetPickerState awps(mParentWidget);
648 MOZ_SEH_TRY {
649 if (aType == PICKER_TYPE_OPEN)
650 result = ::GetOpenFileNameW(ofn);
651 else if (aType == PICKER_TYPE_SAVE)
652 result = ::GetSaveFileNameW(ofn);
653 } MOZ_SEH_EXCEPT(true) {
654 NS_ERROR("nsFilePicker GetFileName win32 call generated an exception! This is bad!");
656 return result;
659 bool
660 nsFilePicker::ShowXPFilePicker(const nsString& aInitialDir)
662 OPENFILENAMEW ofn = {0};
663 ofn.lStructSize = sizeof(ofn);
664 nsString filterBuffer = mFilterList;
666 nsAutoArrayPtr<wchar_t> fileBuffer(new wchar_t[FILE_BUFFER_SIZE]);
667 wcsncpy(fileBuffer, mDefaultFilePath.get(), FILE_BUFFER_SIZE);
668 fileBuffer[FILE_BUFFER_SIZE-1] = '\0'; // null terminate in case copy truncated
670 if (!aInitialDir.IsEmpty()) {
671 ofn.lpstrInitialDir = aInitialDir.get();
674 AutoDestroyTmpWindow adtw((HWND) (mParentWidget.get() ?
675 mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr));
677 ofn.lpstrTitle = (LPCWSTR)mTitle.get();
678 ofn.lpstrFilter = (LPCWSTR)filterBuffer.get();
679 ofn.nFilterIndex = mSelectedType;
680 ofn.lpstrFile = fileBuffer;
681 ofn.nMaxFile = FILE_BUFFER_SIZE;
682 ofn.hwndOwner = adtw.get();
683 ofn.lCustData = reinterpret_cast<LPARAM>(this);
684 ofn.Flags = OFN_SHAREAWARE | OFN_LONGNAMES | OFN_OVERWRITEPROMPT |
685 OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_ENABLESIZING |
686 OFN_EXPLORER;
688 // Windows Vista and up won't allow you to use the new looking dialogs with
689 // a hook procedure. The hook procedure fixes a problem on XP dialogs for
690 // file picker visibility. Vista and up automatically ensures the file
691 // picker is always visible.
692 if (!IsVistaOrLater()) {
693 ofn.lpfnHook = FilePickerHook;
694 ofn.Flags |= OFN_ENABLEHOOK;
697 // Handle add to recent docs settings
698 if (IsPrivacyModeEnabled() || !mAddToRecentDocs) {
699 ofn.Flags |= OFN_DONTADDTORECENT;
702 NS_NAMED_LITERAL_STRING(htmExt, "html");
704 if (!mDefaultExtension.IsEmpty()) {
705 ofn.lpstrDefExt = mDefaultExtension.get();
706 } else if (IsDefaultPathHtml()) {
707 // Get file extension from suggested filename to detect if we are
708 // saving an html file.
709 // This is supposed to append ".htm" if user doesn't supply an
710 // extension but the behavior is sort of weird:
711 // - Often appends ".html" even if you have an extension
712 // - It obeys your extension if you put quotes around name
713 ofn.lpstrDefExt = htmExt.get();
716 // When possible, instead of using OFN_NOCHANGEDIR to ensure the current
717 // working directory will not change from this call, we will retrieve the
718 // current working directory before the call and restore it after the
719 // call. This flag causes problems on Windows XP for paths that are
720 // selected like C:test.txt where the user is currently at C:\somepath
721 // In which case expected result should be C:\somepath\test.txt
722 AutoRestoreWorkingPath restoreWorkingPath;
723 // If we can't get the current working directory, the best case is to
724 // use the OFN_NOCHANGEDIR flag
725 if (!restoreWorkingPath.HasWorkingPath()) {
726 ofn.Flags |= OFN_NOCHANGEDIR;
729 bool result = false;
731 switch(mMode) {
732 case modeOpen:
733 // FILE MUST EXIST!
734 ofn.Flags |= OFN_FILEMUSTEXIST;
735 result = FilePickerWrapper(&ofn, PICKER_TYPE_OPEN);
736 break;
738 case modeOpenMultiple:
739 ofn.Flags |= OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT;
741 // The hook set here ensures that the buffer returned will always be
742 // large enough to hold all selected files. The hook may modify the
743 // value of ofn.lpstrFile and deallocate the old buffer that it pointed
744 // to (fileBuffer). The hook assumes that the passed in value is heap
745 // allocated and that the returned value should be freed by the caller.
746 // If the hook changes the buffer, it will deallocate the old buffer.
747 // This fix would be nice to have in Vista and up, but it would force
748 // the file picker to use the old style dialogs because hooks are not
749 // allowed in the new file picker UI. We need to eventually move to
750 // the new Common File Dialogs for Vista and up.
751 if (!IsVistaOrLater()) {
752 ofn.lpfnHook = MultiFilePickerHook;
753 fileBuffer.forget();
754 result = FilePickerWrapper(&ofn, PICKER_TYPE_OPEN);
755 fileBuffer = ofn.lpstrFile;
756 } else {
757 result = FilePickerWrapper(&ofn, PICKER_TYPE_OPEN);
759 break;
761 case modeSave:
763 ofn.Flags |= OFN_NOREADONLYRETURN;
765 // Don't follow shortcuts when saving a shortcut, this can be used
766 // to trick users (bug 271732)
767 if (IsDefaultPathLink())
768 ofn.Flags |= OFN_NODEREFERENCELINKS;
770 result = FilePickerWrapper(&ofn, PICKER_TYPE_SAVE);
771 if (!result) {
772 // Error, find out what kind.
773 if (GetLastError() == ERROR_INVALID_PARAMETER ||
774 CommDlgExtendedError() == FNERR_INVALIDFILENAME) {
775 // Probably the default file name is too long or contains illegal
776 // characters. Try again, without a starting file name.
777 ofn.lpstrFile[0] = L'\0';
778 result = FilePickerWrapper(&ofn, PICKER_TYPE_SAVE);
782 break;
784 default:
785 NS_NOTREACHED("unsupported file picker mode");
786 return false;
789 if (!result)
790 return false;
792 // Remember what filter type the user selected
793 mSelectedType = (int16_t)ofn.nFilterIndex;
795 // Single file selection, we're done
796 if (mMode != modeOpenMultiple) {
797 GetQualifiedPath(fileBuffer, mUnicodeFile);
798 return true;
801 // Set user-selected location of file or directory. From msdn's "Open and
802 // Save As Dialog Boxes" section:
803 // If you specify OFN_EXPLORER, the directory and file name strings are '\0'
804 // separated, with an extra '\0' character after the last file name. This
805 // format enables the Explorer-style dialog boxes to return long file names
806 // that include spaces.
807 wchar_t *current = fileBuffer;
809 nsAutoString dirName(current);
810 // Sometimes dirName contains a trailing slash and sometimes it doesn't:
811 if (current[dirName.Length() - 1] != '\\')
812 dirName.Append((char16_t)'\\');
814 while (current && *current && *(current + wcslen(current) + 1)) {
815 current = current + wcslen(current) + 1;
817 nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1");
818 NS_ENSURE_TRUE(file, false);
820 // Only prepend the directory if the path specified is a relative path
821 nsAutoString path;
822 if (PathIsRelativeW(current)) {
823 path = dirName + nsDependentString(current);
824 } else {
825 path = current;
828 nsAutoString canonicalizedPath;
829 GetQualifiedPath(path.get(), canonicalizedPath);
830 if (NS_FAILED(file->InitWithPath(canonicalizedPath)) ||
831 !mFiles.AppendObject(file))
832 return false;
835 // Handle the case where the user selected just one file. From msdn: If you
836 // specify OFN_ALLOWMULTISELECT and the user selects only one file the
837 // lpstrFile string does not have a separator between the path and file name.
838 if (current && *current && (current == fileBuffer)) {
839 nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1");
840 NS_ENSURE_TRUE(file, false);
842 nsAutoString canonicalizedPath;
843 GetQualifiedPath(current, canonicalizedPath);
844 if (NS_FAILED(file->InitWithPath(canonicalizedPath)) ||
845 !mFiles.AppendObject(file))
846 return false;
849 return true;
853 * Show a file picker post Windows XP
855 * @param aInitialDir The initial directory, the last used directory will be
856 * used if left blank.
857 * @param aWasInitError Out parameter will hold true if there was an error
858 * before the file picker is shown.
859 * @return true if a file was selected successfully.
861 bool
862 nsFilePicker::ShowFilePicker(const nsString& aInitialDir, bool &aWasInitError)
864 nsRefPtr<IFileDialog> dialog;
865 if (mMode != modeSave) {
866 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC,
867 IID_IFileOpenDialog,
868 getter_AddRefs(dialog)))) {
869 aWasInitError = true;
870 return false;
872 } else {
873 if (FAILED(CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC,
874 IID_IFileSaveDialog,
875 getter_AddRefs(dialog)))) {
876 aWasInitError = true;
877 return false;
880 aWasInitError = false;
882 // hook up event callbacks
883 dialog->Advise(this, &mFDECookie);
885 // options
887 FILEOPENDIALOGOPTIONS fos = 0;
888 fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT |
889 FOS_FORCEFILESYSTEM;
891 // Handle add to recent docs settings
892 if (IsPrivacyModeEnabled() || !mAddToRecentDocs) {
893 fos |= FOS_DONTADDTORECENT;
896 // Msdn claims FOS_NOCHANGEDIR is not needed. We'll add this
897 // just in case.
898 AutoRestoreWorkingPath arw;
900 // mode specific
901 switch(mMode) {
902 case modeOpen:
903 fos |= FOS_FILEMUSTEXIST;
904 break;
906 case modeOpenMultiple:
907 fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT;
908 break;
910 case modeSave:
911 fos |= FOS_NOREADONLYRETURN;
912 // Don't follow shortcuts when saving a shortcut, this can be used
913 // to trick users (bug 271732)
914 if (IsDefaultPathLink())
915 fos |= FOS_NODEREFERENCELINKS;
916 break;
919 dialog->SetOptions(fos);
921 // initial strings
923 // title
924 dialog->SetTitle(mTitle.get());
926 // default filename
927 if (!mDefaultFilename.IsEmpty()) {
928 dialog->SetFileName(mDefaultFilename.get());
931 NS_NAMED_LITERAL_STRING(htmExt, "html");
933 // default extension to append to new files
934 if (!mDefaultExtension.IsEmpty()) {
935 dialog->SetDefaultExtension(mDefaultExtension.get());
936 } else if (IsDefaultPathHtml()) {
937 dialog->SetDefaultExtension(htmExt.get());
940 // initial location
941 if (!aInitialDir.IsEmpty()) {
942 nsRefPtr<IShellItem> folder;
943 if (SUCCEEDED(
944 WinUtils::SHCreateItemFromParsingName(aInitialDir.get(), nullptr,
945 IID_IShellItem,
946 getter_AddRefs(folder)))) {
947 dialog->SetFolder(folder);
951 // filter types and the default index
952 if (!mComFilterList.IsEmpty()) {
953 dialog->SetFileTypes(mComFilterList.Length(), mComFilterList.get());
954 dialog->SetFileTypeIndex(mSelectedType);
957 // display
960 AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ?
961 mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr));
962 AutoTimerCallbackCancel atcc(this, PickerCallbackTimerFunc);
963 AutoWidgetPickerState awps(mParentWidget);
965 if (FAILED(dialog->Show(adtw.get()))) {
966 dialog->Unadvise(mFDECookie);
967 return false;
969 dialog->Unadvise(mFDECookie);
972 // results
974 // Remember what filter type the user selected
975 UINT filterIdxResult;
976 if (SUCCEEDED(dialog->GetFileTypeIndex(&filterIdxResult))) {
977 mSelectedType = (int16_t)filterIdxResult;
980 // single selection
981 if (mMode != modeOpenMultiple) {
982 nsRefPtr<IShellItem> item;
983 if (FAILED(dialog->GetResult(getter_AddRefs(item))) || !item)
984 return false;
985 return WinUtils::GetShellItemPath(item, mUnicodeFile);
988 // multiple selection
989 nsRefPtr<IFileOpenDialog> openDlg;
990 dialog->QueryInterface(IID_IFileOpenDialog, getter_AddRefs(openDlg));
991 if (!openDlg) {
992 // should not happen
993 return false;
996 nsRefPtr<IShellItemArray> items;
997 if (FAILED(openDlg->GetResults(getter_AddRefs(items))) || !items) {
998 return false;
1001 DWORD count = 0;
1002 items->GetCount(&count);
1003 for (unsigned int idx = 0; idx < count; idx++) {
1004 nsRefPtr<IShellItem> item;
1005 nsAutoString str;
1006 if (SUCCEEDED(items->GetItemAt(idx, getter_AddRefs(item)))) {
1007 if (!WinUtils::GetShellItemPath(item, str))
1008 continue;
1009 nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1");
1010 if (file && NS_SUCCEEDED(file->InitWithPath(str)))
1011 mFiles.AppendObject(file);
1014 return true;
1017 ///////////////////////////////////////////////////////////////////////////////
1018 // nsIFilePicker impl.
1020 NS_IMETHODIMP
1021 nsFilePicker::ShowW(int16_t *aReturnVal)
1023 NS_ENSURE_ARG_POINTER(aReturnVal);
1025 *aReturnVal = returnCancel;
1027 AutoSuppressEvents supress(mParentWidget);
1029 nsAutoString initialDir;
1030 if (mDisplayDirectory)
1031 mDisplayDirectory->GetPath(initialDir);
1033 // If no display directory, re-use the last one.
1034 if(initialDir.IsEmpty()) {
1035 // Allocate copy of last used dir.
1036 initialDir = mLastUsedUnicodeDirectory;
1039 // Clear previous file selections
1040 mUnicodeFile.Truncate();
1041 mFiles.Clear();
1043 // Launch the XP file/folder picker on XP and as a fallback on Vista+.
1044 // The CoCreateInstance call to CLSID_FileOpenDialog fails with "(0x80040111)
1045 // ClassFactory cannot supply requested class" when the checkbox for
1046 // Disable Visual Themes is on in the compatability tab within the shortcut
1047 // properties.
1048 bool result = false, wasInitError = true;
1049 if (mMode == modeGetFolder) {
1050 if (IsVistaOrLater())
1051 result = ShowFolderPicker(initialDir, wasInitError);
1052 if (!result && wasInitError)
1053 result = ShowXPFolderPicker(initialDir);
1054 } else {
1055 if (IsVistaOrLater())
1056 result = ShowFilePicker(initialDir, wasInitError);
1057 if (!result && wasInitError)
1058 result = ShowXPFilePicker(initialDir);
1061 // exit, and return returnCancel in aReturnVal
1062 if (!result)
1063 return NS_OK;
1065 RememberLastUsedDirectory();
1067 int16_t retValue = returnOK;
1068 if (mMode == modeSave) {
1069 // Windows does not return resultReplace, we must check if file
1070 // already exists.
1071 nsCOMPtr<nsIFile> file(do_CreateInstance("@mozilla.org/file/local;1"));
1072 bool flag = false;
1073 if (file && NS_SUCCEEDED(file->InitWithPath(mUnicodeFile)) &&
1074 NS_SUCCEEDED(file->Exists(&flag)) && flag) {
1075 retValue = returnReplace;
1079 *aReturnVal = retValue;
1080 return NS_OK;
1083 NS_IMETHODIMP
1084 nsFilePicker::Show(int16_t *aReturnVal)
1086 return ShowW(aReturnVal);
1089 NS_IMETHODIMP
1090 nsFilePicker::GetFile(nsIFile **aFile)
1092 NS_ENSURE_ARG_POINTER(aFile);
1093 *aFile = nullptr;
1095 if (mUnicodeFile.IsEmpty())
1096 return NS_OK;
1098 nsCOMPtr<nsIFile> file(do_CreateInstance("@mozilla.org/file/local;1"));
1100 NS_ENSURE_TRUE(file, NS_ERROR_FAILURE);
1102 file->InitWithPath(mUnicodeFile);
1104 NS_ADDREF(*aFile = file);
1106 return NS_OK;
1109 NS_IMETHODIMP
1110 nsFilePicker::GetFileURL(nsIURI **aFileURL)
1112 *aFileURL = nullptr;
1113 nsCOMPtr<nsIFile> file;
1114 nsresult rv = GetFile(getter_AddRefs(file));
1115 if (!file)
1116 return rv;
1118 return NS_NewFileURI(aFileURL, file);
1121 NS_IMETHODIMP
1122 nsFilePicker::GetFiles(nsISimpleEnumerator **aFiles)
1124 NS_ENSURE_ARG_POINTER(aFiles);
1125 return NS_NewArrayEnumerator(aFiles, mFiles);
1128 // Get the file + path
1129 NS_IMETHODIMP
1130 nsBaseWinFilePicker::SetDefaultString(const nsAString& aString)
1132 mDefaultFilePath = aString;
1134 // First, make sure the file name is not too long.
1135 int32_t nameLength;
1136 int32_t nameIndex = mDefaultFilePath.RFind("\\");
1137 if (nameIndex == kNotFound)
1138 nameIndex = 0;
1139 else
1140 nameIndex ++;
1141 nameLength = mDefaultFilePath.Length() - nameIndex;
1142 mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex));
1144 if (nameLength > MAX_PATH) {
1145 int32_t extIndex = mDefaultFilePath.RFind(".");
1146 if (extIndex == kNotFound)
1147 extIndex = mDefaultFilePath.Length();
1149 // Let's try to shave the needed characters from the name part.
1150 int32_t charsToRemove = nameLength - MAX_PATH;
1151 if (extIndex - nameIndex >= charsToRemove) {
1152 mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove);
1156 // Then, we need to replace illegal characters. At this stage, we cannot
1157 // replace the backslash as the string might represent a file path.
1158 mDefaultFilePath.ReplaceChar(FILE_ILLEGAL_CHARACTERS, '-');
1159 mDefaultFilename.ReplaceChar(FILE_ILLEGAL_CHARACTERS, '-');
1161 return NS_OK;
1164 NS_IMETHODIMP
1165 nsBaseWinFilePicker::GetDefaultString(nsAString& aString)
1167 return NS_ERROR_FAILURE;
1170 // The default extension to use for files
1171 NS_IMETHODIMP
1172 nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension)
1174 aExtension = mDefaultExtension;
1175 return NS_OK;
1178 NS_IMETHODIMP
1179 nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension)
1181 mDefaultExtension = aExtension;
1182 return NS_OK;
1185 // Set the filter index
1186 NS_IMETHODIMP
1187 nsFilePicker::GetFilterIndex(int32_t *aFilterIndex)
1189 // Windows' filter index is 1-based, we use a 0-based system.
1190 *aFilterIndex = mSelectedType - 1;
1191 return NS_OK;
1194 NS_IMETHODIMP
1195 nsFilePicker::SetFilterIndex(int32_t aFilterIndex)
1197 // Windows' filter index is 1-based, we use a 0-based system.
1198 mSelectedType = aFilterIndex + 1;
1199 return NS_OK;
1202 void
1203 nsFilePicker::InitNative(nsIWidget *aParent,
1204 const nsAString& aTitle)
1206 mParentWidget = aParent;
1207 mTitle.Assign(aTitle);
1210 void
1211 nsFilePicker::GetQualifiedPath(const wchar_t *aInPath, nsString &aOutPath)
1213 // Prefer a qualified path over a non qualified path.
1214 // Things like c:file.txt would be accepted in Win XP but would later
1215 // fail to open from the download manager.
1216 wchar_t qualifiedFileBuffer[MAX_PATH];
1217 if (PathSearchAndQualifyW(aInPath, qualifiedFileBuffer, MAX_PATH)) {
1218 aOutPath.Assign(qualifiedFileBuffer);
1219 } else {
1220 aOutPath.Assign(aInPath);
1224 void
1225 nsFilePicker::AppendXPFilter(const nsAString& aTitle, const nsAString& aFilter)
1227 mFilterList.Append(aTitle);
1228 mFilterList.Append(char16_t('\0'));
1230 if (aFilter.EqualsLiteral("..apps"))
1231 mFilterList.AppendLiteral("*.exe;*.com");
1232 else
1234 nsAutoString filter(aFilter);
1235 filter.StripWhitespace();
1236 if (filter.EqualsLiteral("*"))
1237 filter.AppendLiteral(".*");
1238 mFilterList.Append(filter);
1241 mFilterList.Append(char16_t('\0'));
1244 NS_IMETHODIMP
1245 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter)
1247 if (IsVistaOrLater()) {
1248 mComFilterList.Append(aTitle, aFilter);
1249 } else {
1250 AppendXPFilter(aTitle, aFilter);
1252 return NS_OK;
1255 void
1256 nsFilePicker::RememberLastUsedDirectory()
1258 nsCOMPtr<nsIFile> file(do_CreateInstance("@mozilla.org/file/local;1"));
1259 if (!file || NS_FAILED(file->InitWithPath(mUnicodeFile))) {
1260 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
1261 return;
1264 nsCOMPtr<nsIFile> dir;
1265 nsAutoString newDir;
1266 if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) ||
1267 !(mDisplayDirectory = do_QueryInterface(dir)) ||
1268 NS_FAILED(mDisplayDirectory->GetPath(newDir)) ||
1269 newDir.IsEmpty()) {
1270 NS_WARNING("RememberLastUsedDirectory failed to get parent directory.");
1271 return;
1274 if (mLastUsedUnicodeDirectory) {
1275 NS_Free(mLastUsedUnicodeDirectory);
1276 mLastUsedUnicodeDirectory = nullptr;
1278 mLastUsedUnicodeDirectory = ToNewUnicode(newDir);
1281 bool
1282 nsFilePicker::IsPrivacyModeEnabled()
1284 return mLoadContext && mLoadContext->UsePrivateBrowsing();
1287 bool
1288 nsFilePicker::IsDefaultPathLink()
1290 NS_ConvertUTF16toUTF8 ext(mDefaultFilePath);
1291 ext.Trim(" .", false, true); // watch out for trailing space and dots
1292 ToLowerCase(ext);
1293 if (StringEndsWith(ext, NS_LITERAL_CSTRING(".lnk")) ||
1294 StringEndsWith(ext, NS_LITERAL_CSTRING(".pif")) ||
1295 StringEndsWith(ext, NS_LITERAL_CSTRING(".url")))
1296 return true;
1297 return false;
1300 bool
1301 nsFilePicker::IsDefaultPathHtml()
1303 int32_t extIndex = mDefaultFilePath.RFind(".");
1304 if (extIndex >= 0) {
1305 nsAutoString ext;
1306 mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex);
1307 if (ext.LowerCaseEqualsLiteral(".htm") ||
1308 ext.LowerCaseEqualsLiteral(".html") ||
1309 ext.LowerCaseEqualsLiteral(".shtml"))
1310 return true;
1312 return false;
1315 void
1316 nsFilePicker::ComDlgFilterSpec::Append(const nsAString& aTitle, const nsAString& aFilter)
1318 COMDLG_FILTERSPEC* pSpecForward = mSpecList.AppendElement();
1319 if (!pSpecForward) {
1320 NS_WARNING("mSpecList realloc failed.");
1321 return;
1323 memset(pSpecForward, 0, sizeof(*pSpecForward));
1324 nsString* pStr = mStrings.AppendElement(aTitle);
1325 if (!pStr) {
1326 NS_WARNING("mStrings.AppendElement failed.");
1327 return;
1329 pSpecForward->pszName = pStr->get();
1330 pStr = mStrings.AppendElement(aFilter);
1331 if (!pStr) {
1332 NS_WARNING("mStrings.AppendElement failed.");
1333 return;
1335 if (aFilter.EqualsLiteral("..apps"))
1336 pStr->AssignLiteral("*.exe;*.com");
1337 else {
1338 pStr->StripWhitespace();
1339 if (pStr->EqualsLiteral("*"))
1340 pStr->AppendLiteral(".*");
1342 pSpecForward->pszSpec = pStr->get();